📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What is the difference between a .sln and a .slnx solution file, and when would you use a solution filter?

Solutions versus projects, the classic .sln format, the XML .slnx format that is the default for new solutions in the .NET 10 SDK, migrating with dotnet sln migrate, and .slnf solution filters for large repos.

Junior Theory Beginner project-system slnx solution-files visual-studio

🎯 Key points

  • A solution groups projects for IDEs and the CLI; the project file, not the solution, defines how each project builds
  • The classic .sln is a custom text format with GUIDs and per-configuration mappings, which causes merge conflicts
  • .slnx is an XML format that lists projects and folders in a few lines and is far easier to review and merge
  • The .NET 10 SDK creates .slnx by default with dotnet new sln; dotnet sln migrate converts an existing .sln
  • Check tool support before switching: current IDEs and the .NET 9.0.200+ SDK read .slnx, but older tools may not
  • A solution filter (.slnf) opens a subset of a solution's projects, which keeps large repos fast in the IDE
  • CI can build a solution, a filter, a project, or a traversal project

⏱ 30-second answer

A solution is a list of projects that IDEs open together and the CLI can build together. Each project's build is still defined by its own project file. The classic .sln is a custom text format full of GUIDs and configuration mappings, so it's hard to read and often conflicts in merges. .slnx is a short XML format that just lists projects and folders. The .NET 10 SDK creates .slnx by default, and dotnet sln migrate converts an existing .sln. A solution filter (.slnf) opens only some of a solution's projects. That's useful in a big repo when a team works on a slice of it.

🗣 Full answer (senior)

How I would answer this in an interview

"A solution is a list of projects that IDEs open together and the CLI can build together, plus mappings from solution configurations to project configurations. It doesn't define how anything builds: that lives in each project file and in Directory.Build.props, so every project should still build on its own.

The classic .sln is a custom text format full of GUIDs and per-configuration lines, so each new project adds lines in several places and two branches that add projects almost always conflict. .slnx is a short XML format: one line per project, folders as elements, and standard mappings implied. The .NET 10 SDK creates .slnx by default with dotnet new sln, dotnet sln migrate converts an existing file, and dotnet sln add works with either.

Before switching, I check every tool that reads solution files: current IDEs and the .NET SDK from 9.0.200 support .slnx, but older scripts may not. The mistake I see is keeping both files, so people add projects to one but not the other. A solution filter, a .slnf file, opens just a subset of a solution's projects, which keeps a large monorepo fast in the IDE for each team. CI can build a solution, a filter, a single project or a traversal project, and I keep solutions thin: a view over projects, never the place where build behaviour lives."


What a solution is for

A solution is an organising file. IDEs use it to know which projects to load, and dotnet build Orders.slnx builds every project in it. It also maps solution configurations (Debug/Release) to project configurations. It does not contain build logic; that's in the project files and Directory.Build.* files.

Classic .sln

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orders.Api", "src\Orders.Api\Orders.Api.csproj", "{6A1C...}"
EndProject
Global
  GlobalSection(ProjectConfigurationPlatforms) = postSolution
    {6A1C...}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
    ...

Every project adds several GUID lines, and adding two projects on two branches almost always conflicts.

.slnx

<Solution>
  <Folder Name="/src/">
    <Project Path="src/Orders.Api/Orders.Api.csproj" />
    <Project Path="src/Orders.Domain/Orders.Domain.csproj" />
  </Folder>
  <Folder Name="/tests/">
    <Project Path="tests/Orders.Tests/Orders.Tests.csproj" />
  </Folder>
</Solution>

Standard configuration mappings are implied, so only exceptions need writing down.

  • Creating: in the .NET 10 SDK, dotnet new sln creates a .slnx; dotnet new sln --format sln gives the classic format.
  • Converting: dotnet sln Orders.sln migrate writes Orders.slnx next to it. Remove the old .sln once everything uses the new one.
  • Support: the .NET SDK from 9.0.200, current Visual Studio and Rider, and the VS Code C# Dev Kit read .slnx. Check any other tool that parses solution files (older build scripts, some code-analysis tools) before you switch.

Solution filters (.slnf)

A .slnf is a small JSON file that points to a solution and lists the projects to load:

{
  "solution": {
    "path": "Contoso.slnx",
    "projects": [
      "src\\Payments\\Payments.Api\\Payments.Api.csproj",
      "src\\Payments\\Payments.Domain\\Payments.Domain.csproj",
      "tests\\Payments.Tests\\Payments.Tests.csproj"
    ]
  }
}

Teams open their filter instead of the 300-project solution, and the IDE loads in seconds. dotnet build Payments.slnf builds just that set. Visual Studio can create filters from Solution Explorer (unload projects, then save as a filter).

Production example

A monorepo's .sln with 180 projects caused a merge conflict in most pull requests that added a project. Migrating to .slnx cut the file from about 1,400 lines to 200 and conflicts almost stopped. Each team also got a .slnf, which brought Visual Studio load time from over a minute to under 15 seconds.

🔍 What the interviewer is checking

Checks practical knowledge of solution files and large-repo hygiene in a team setting.

Green flags: knows solutions don't define build logic; explains why .sln causes merge conflicts when many people add projects and how .slnx fixes that; knows .slnx is the default for new solutions in the .NET 10 SDK and when to keep .sln for compatibility; uses solution filters to keep large repos workable for each team.

Red flags: thinks the solution is required to build a project; has never heard of .slnx or .slnf; resolves solution conflicts by guesswork.

📚 Learn in depth

1. Where the solution sits

        IDE / dotnet build
               |
               v
   +-------------------------+        +----------------------+
   | Contoso.slnx            |        | Payments.slnf        |
   |  list of projects       |<-------|  subset of projects  |
   |  folders, config maps   |        +----------------------+
   +-------------------------+
               |
     +---------+----------+
     v         v          v
  Api.csproj Domain.csproj Tests.csproj   <-- build logic lives here
     \         |          /
      Directory.Build.props / .targets     <-- and here

A solution answers two questions: which projects belong together, and which project configuration to build for each solution configuration (for example, solution Release|Any CPU builds every project as Release|AnyCPU). Everything else (target framework, packages, compiler settings) lives in project files.

The quick test: delete the solution, and every project should still build with dotnet build path/to/project.csproj. If it doesn't, build logic has leaked into the solution, usually through $(SolutionDir) (see section 6).

2. How MSBuild builds a solution

MSBuild doesn't understand solution files natively. When you run dotnet build Contoso.slnx (or .sln), it converts the solution into an in-memory metaproject: a generated MSBuild project with one MSBuild task call per project, passing the mapped Configuration and Platform.

 Contoso.slnx --parse--> metaproject (in memory)
                            |
                            +-- MSBuild Api.csproj      Configuration=Release
                            +-- MSBuild Domain.csproj   Configuration=Release
                            +-- MSBuild Tests.csproj    Configuration=Release
                                   |
                                   +-- ProjectReference --> built on demand

Set the environment variable MSBUILDEMITSOLUTION=1 to write the metaproject to disk as Contoso.slnx.metaproj and see exactly what it does. Build order comes from ProjectReference items, not from the order in the solution.

3. The two formats compared

.sln .slnx
Format Custom text format from 2002 XML
Project entries Type GUID, name, path and project GUID per project <Project Path="..." />
Configuration mappings One line per project per configuration per platform Implied for the standard case; only exceptions are written down
Solution folders Fake "projects" with their own GUID and a NestedProjects section <Folder Name="/src/"> elements
Size, 100 projects Often more than 1,000 lines Roughly 100 to 150 lines
Merge conflicts Very common when two branches add projects Rare, and easy to resolve
Supported by Every tool .NET SDK 9.0.200 and later, MSBuild 17.13 and later, current Visual Studio, Rider and VS Code C# Dev Kit
Default for dotnet new sln Up to the .NET 9 SDK From the .NET 10 SDK

What a .sln entry really contains

Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orders.Api", "src\Orders.Api\Orders.Api.csproj", "{6A1C4F0E-...}"
EndProject
...
Global
  GlobalSection(ProjectConfigurationPlatforms) = postSolution
    {6A1C4F0E-...}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
    {6A1C4F0E-...}.Debug|Any CPU.Build.0 = Debug|Any CPU
    {6A1C4F0E-...}.Release|Any CPU.ActiveCfg = Release|Any CPU
    {6A1C4F0E-...}.Release|Any CPU.Build.0 = Release|Any CPU
  EndGlobalSection
  GlobalSection(NestedProjects) = preSolution
    {6A1C4F0E-...} = {2150E333-...}
  EndGlobalSection
EndGlobal

The first GUID is the project type (C# SDK-style, in this case). The second is the project's own id. Each new project adds lines in three different places, which is why two branches that each add a project almost always conflict.

The same thing in .slnx

<Solution>
  <Folder Name="/Solution Items/">
    <File Path="Directory.Build.props" />
    <File Path="global.json" />
  </Folder>
  <Folder Name="/src/">
    <Project Path="src/Orders.Api/Orders.Api.csproj" />
    <Project Path="src/Orders.Domain/Orders.Domain.csproj" />
  </Folder>
  <Folder Name="/tests/">
    <Project Path="tests/Orders.Tests/Orders.Tests.csproj" />
  </Folder>
</Solution>

No GUIDs, forward slashes in paths, and one line per project. Non-standard mappings, such as a project that should only build for one platform, are written as child elements of that <Project>.

4. Migrating from .sln to .slnx

  1. List every tool that reads solution files: CI scripts, code-analysis tools, documentation generators, internal scripts that parse the .sln text. Check each supports .slnx.
  2. Make sure everyone has a supporting IDE version and an SDK of 9.0.200 or later (pinned in global.json).
  3. Run dotnet sln Contoso.sln migrate. It writes Contoso.slnx next to the original.
  4. Build and test with the new file locally and in CI (dotnet build Contoso.slnx). Compare the list of projects with dotnet sln Contoso.slnx list.
  5. Delete Contoso.sln in the same pull request, update CI paths and any .slnf files that point at the old name.
  6. Announce the change, because open branches that add projects need to add them to the new file.

5. Solution filters (.slnf)

{
  "solution": {
    "path": "Contoso.slnx",
    "projects": [
      "src\\Payments\\Payments.Api\\Payments.Api.csproj",
      "src\\Payments\\Payments.Domain\\Payments.Domain.csproj",
      "tests\\Payments.Tests\\Payments.Tests.csproj"
    ]
  }
}
Behaviour Detail
In the IDE Only the listed projects load. Others appear unloaded and cost nothing
With the CLI dotnet build Payments.slnf builds the listed projects
Project references MSBuild still builds projects the listed ones reference, even if they aren't in the filter. A filter limits what you open, not what the build needs
Creating one In Visual Studio, unload the projects you don't need, then "Save As Solution Filter" from the solution's context menu; or write the JSON by hand
Path solution.path is relative to the .slnf file, so keep filters next to the solution

Filters are a developer convenience. CI usually still builds the full solution, or uses a traversal project, so nothing is missed.

6. Solution-only properties to avoid

When MSBuild builds through a solution, it sets properties such as SolutionDir, SolutionName and SolutionPath. When you build a single project, they are undefined (SolutionDir becomes *Undefined*).

<!-- Breaks when the project is built on its own -->
<Import Project="$(SolutionDir)build\common.props" />

Instead: locate shared files relative to the project, for example with $([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), 'global.json')), or move the settings into Directory.Build.props, which MSBuild finds automatically.

7. Traversal projects for CI

Very large repos often stop relying on a hand-maintained solution in CI:

<!-- dirs.proj at the repo root -->
<Project Sdk="Microsoft.Build.Traversal">
  <ItemGroup>
    <ProjectReference Include="src/**/*.csproj" />
    <ProjectReference Include="tests/**/*.csproj" />
  </ItemGroup>
</Project>

Pin the SDK version in global.json under msbuild-sdks. dotnet build dirs.proj then builds every project found by the globs, so a new project can't be forgotten. Solutions and filters remain for the IDE.

Option Best for
One solution Small and medium repos
Solution plus team filters Large repos where teams own slices
Traversal project in CI, solutions or filters in the IDE Monorepos with hundreds of projects

8. Performance

  • IDE load time grows with the number of loaded projects. A filter with 15 projects instead of 300 often opens in seconds rather than minutes.
  • Build time doesn't change with the file format; .sln and .slnx produce the same metaproject.
  • Large solutions in CI benefit more from incremental builds, caching and parallelism (-m) than from the solution format.

9. .NET 11 awareness

Expect .slnx to remain the default and more third-party tools to support it. Keep production repos on the .NET 10 SDK; the solution format needs no change for .NET 11.

10. Try it yourself

  1. Create a classic solution: dotnet new sln --format sln -n Demo, then add two projects with dotnet sln Demo.sln add.
  2. Open Demo.sln and count the lines and GUIDs.
  3. Run dotnet sln Demo.sln migrate and open Demo.slnx.
  4. Run dotnet build Demo.slnx and dotnet sln Demo.slnx list.
  5. Set MSBUILDEMITSOLUTION=1, build again and open the generated .metaproj file.
  6. Write a .slnf that lists one project which references the other, run dotnet build Demo.slnf, and notice the referenced project builds too.
  7. Add <Import Project="$(SolutionDir)x.props" Condition="Exists('$(SolutionDir)x.props')" /> to a project, then build the project alone and through the solution, and compare.

💻 Code

# New solution (slnx by default in the .NET 10 SDK)
dotnet new sln -n Contoso
dotnet sln Contoso.slnx add src/Orders.Api/Orders.Api.csproj src/Orders.Domain/Orders.Domain.csproj
dotnet sln Contoso.slnx add --solution-folder tests tests/Orders.Tests/Orders.Tests.csproj
dotnet sln Contoso.slnx list

# Convert an existing classic solution
dotnet sln Legacy.sln migrate

# Build just a team's slice
dotnet build Payments.slnf -c Release

⚠ Common mistakes

  • Mistake: putting build behaviour in the solution. Relying on solution-only configuration mappings or $(SolutionDir) means a project builds differently on its own than in the solution, and breaks when a filter or a single-project build is used. Instead: keep all build logic in project files and Directory.Build.props / .targets, and check that each project builds with dotnet build path/to/project.csproj.

  • Mistake: living with constant .sln merge conflicts. Each project adds lines in several places (project entry, configuration mappings, nested folders), so two branches that each add a project almost always conflict. Resolving them by hand can silently drop a project or a mapping. Instead: migrate to .slnx with dotnet sln <name>.sln migrate, where each project is one line.

  • Mistake: switching to .slnx without checking every tool. An old script, an older IDE version or a third-party tool that parses .sln files fails, often only in CI or on one team's machines. Instead: list every consumer of the solution file first, require an SDK of 9.0.200 or later in global.json, and keep .sln (dotnet new sln --format sln) for repos whose tools don't support .slnx yet.

  • Mistake: keeping both .sln and .slnx long-term. People add projects to one file but not the other, so the IDE and CI build different sets of projects. Instead: migrate in one pull request, delete the old .sln in that same pull request and update CI paths.

  • Mistake: opening a 300-project solution every day. The IDE spends minutes loading and indexing projects you never touch, and analysis and IntelliSense slow down. Instead: give each team a solution filter (.slnf) with the projects they work on, kept next to the solution.

  • Mistake: expecting a solution filter to limit what CI builds or tests. CI usually builds the full solution, and MSBuild also builds every project a filtered project references. Instead: treat filters as an IDE convenience. Decide what CI builds separately, with the full solution, a traversal project or explicit project paths.

  • Mistake: assuming the order of projects in the solution sets the build order. MSBuild orders the build by ProjectReference dependencies. Projects that depend on each other only through the solution's build order, or through files copied between output folders, break with parallel builds. Instead: express every real dependency as a ProjectReference.

  • Mistake: forgetting to add a new project to the solution. The project builds when referenced, but its tests never run in CI, or it never appears in the IDE. Instead: add projects with dotnet sln add, check dotnet sln list in review, or use a traversal project with globs in CI so new projects are picked up automatically.

🏛 Senior insight: trade-offs

A senior engineer keeps solutions thin: they're a view over projects, never the place where build behaviour lives. In large repos, they move to .slnx to remove a constant source of merge pain, give each team a solution filter, and consider a traversal project for CI so the build graph doesn't depend on a hand-maintained list. The migration is cheap but has to be coordinated: announce it, check every tool that reads solution files, migrate in one pull request and delete the old .sln.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate says a solution defines how projects build. Why: Confirms the candidate knows build logic lives in project files, not solutions.
    How to answer

    An SDK-style project begins with <Project Sdk="Microsoft.NET.Sdk">. That one attribute pulls in all the standard build logic, so the file only holds what's specific to your project: the target framework, the output type, a few settings and its references. Source files are picked up automatically by globbing, so you don't list each .cs file. Packages are PackageReference items. Old-style .NET Framework project files listed every file, imported targets by hand and used packages.config. That made them long, noisy in merges and tied to Visual Studio on Windows.

  2. When: Candidate mentions solution filters for a large repo. Why: Scales the discussion to governing a large monorepo.
    How to answer

    I'd treat the build as a product, with a contract that lives in files at the repo root:

    • global.json for the SDK.
    • Layered Directory.Build.props/.targets for framework, nullable, analysers and output layout.
    • Directory.Packages.props with transitive pinning for versions.
    • nuget.config with source mapping, plus lock files.
    • .editorconfig for style and analyser severities.

    Project files stay tiny. A small platform group owns those files through CODEOWNERS, and there's a documented exception process for overrides and suppressions. CI uses shared templates that run exactly what developers run locally. The warning policy ratchets stricter over time. SDK, framework and package upgrades arrive as automated, grouped pull requests. I'd measure it all (build time, warnings, drift, time to patch) so governance helps teams instead of just blocking them.

  3. When: Candidate mentions dotnet new sln or dotnet sln add. Why: Checks CLI fluency for creating solutions and adding projects.
    How to answer

    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.

More in Project System

Last updated 1 Nov 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me