19. 12. 2025 Francesco Belacca Microsoft

The New .slnx Format for dotnet Solutions

.sln Is Dead, Long Live .slnx: Why This Tiny Change Matters for Your Pipelines

Some months ago here I used file-based apps and dotnet run app.cs as an excuse to question when C# might realistically replace PowerShell in day-to-day operations.

This time the trigger is less flashy, but just as impactful: the new .slnx solution format quietly arriving in .NET tooling. Microsoft has just introduced an XML-based solution file that the dotnet CLI can create, build, and migrate to, starting from SDK 9.0.200

With .NET 10, dotnet new sln even defaults to this new format, which means teams will start hitting it whether they want to or not. As someone who lives inside Git repos, YAML pipelines, and multi-project solutions, this feels like the right moment to step back and ask a simple question: what does .slnx really change for us in practice?

What Actually Changes with .slnx

Classic .sln files are a proprietary plaintext format that grew organically over decades, accumulating GUIDs, configuration noise, and a structure that very few people understand or want to touch.

The new .slnx format keeps the same conceptual role – describing which projects belong to a solution and how they are grouped – but represents it as a compact XML document that looks much closer to modern SDK-style project files.

At its simplest, an .slnx file is essentially a <Solution> root element with <Project> entries that point to your .csproj files, plus optional configuration metadata, making it dramatically more readable by humans and tools.

From the CLI side, Microsoft has wired this into the usual workflow: you can migrate existing .sln files, build .slnx solutions, and manage projects with the same dotnet sln subcommands you already know.

Why Teams and SREs Should Care

Most complaints about .sln are not about ideology; they are about merge conflicts and maintainability in large teams. A single extra project, configuration change, or Visual Studio extension can spray new GUIDs and boilerplate across the file, making diffs noisy and conflicts painful to resolve.

In the new format, each project reference is a short, stable XML element, so Git diffs tend to be smaller and easier to reason about. That means automated tools (for example, scripts that generate or patch solutions) can treat the file like any other structured XML configuration.

Static analysis vendors like PVS-Studio have already added first-class .slnx support and explicitly extol the improved readability and simplification as a win for both developers and tooling authors.

A Practical Migration Example

To migrate an .sln file, use dotnet sln <solution> migrate. We did this for one of our internal solutions and got it down from 72 lines to just 11 lines of code 🙂

This was the previous content:


Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{2531614F-61A8-474C-8896-EF9A3719691B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JiraTicketAutomator", "src\JiraTicketAutomator\JiraTicketAutomator.csproj", "{30446FC8-83DA-47A7-ADE8-526A87E1AA63}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SolmanTicketAutomator", "src\SolmanTicketAutomator\SolmanTicketAutomator.csproj", "{F31B8A71-B5E2-46B5-801F-D4865D050E5A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UpsertJiraTicket", "src\UpsertJiraTicket\UpsertJiraTicket.csproj", "{DDF7496A-8437-423E-B702-DDF4D081796E}"
EndProject
Global
	GlobalSection(SolutionConfigurationPlatforms) = preSolution
		Debug|Any CPU = Debug|Any CPU
		Debug|x64 = Debug|x64
		Debug|x86 = Debug|x86
		Release|Any CPU = Release|Any CPU
		Release|x64 = Release|x64
		Release|x86 = Release|x86
	EndGlobalSection
	GlobalSection(ProjectConfigurationPlatforms) = postSolution
		{30446FC8-83DA-47A7-ADE8-526A87E1AA63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{30446FC8-83DA-47A7-ADE8-526A87E1AA63}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{30446FC8-83DA-47A7-ADE8-526A87E1AA63}.Debug|x64.ActiveCfg = Debug|Any CPU
		{30446FC8-83DA-47A7-ADE8-526A87E1AA63}.Debug|x64.Build.0 = Debug|Any CPU
		{30446FC8-83DA-47A7-ADE8-526A87E1AA63}.Debug|x86.ActiveCfg = Debug|Any CPU
		{30446FC8-83DA-47A7-ADE8-526A87E1AA63}.Debug|x86.Build.0 = Debug|Any CPU
		{30446FC8-83DA-47A7-ADE8-526A87E1AA63}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{30446FC8-83DA-47A7-ADE8-526A87E1AA63}.Release|Any CPU.Build.0 = Release|Any CPU
		{30446FC8-83DA-47A7-ADE8-526A87E1AA63}.Release|x64.ActiveCfg = Release|Any CPU
		{30446FC8-83DA-47A7-ADE8-526A87E1AA63}.Release|x64.Build.0 = Release|Any CPU
		{30446FC8-83DA-47A7-ADE8-526A87E1AA63}.Release|x86.ActiveCfg = Release|Any CPU
		{30446FC8-83DA-47A7-ADE8-526A87E1AA63}.Release|x86.Build.0 = Release|Any CPU
		{F31B8A71-B5E2-46B5-801F-D4865D050E5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{F31B8A71-B5E2-46B5-801F-D4865D050E5A}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{F31B8A71-B5E2-46B5-801F-D4865D050E5A}.Debug|x64.ActiveCfg = Debug|Any CPU
		{F31B8A71-B5E2-46B5-801F-D4865D050E5A}.Debug|x64.Build.0 = Debug|Any CPU
		{F31B8A71-B5E2-46B5-801F-D4865D050E5A}.Debug|x86.ActiveCfg = Debug|Any CPU
		{F31B8A71-B5E2-46B5-801F-D4865D050E5A}.Debug|x86.Build.0 = Debug|Any CPU
		{F31B8A71-B5E2-46B5-801F-D4865D050E5A}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{F31B8A71-B5E2-46B5-801F-D4865D050E5A}.Release|Any CPU.Build.0 = Release|Any CPU
		{F31B8A71-B5E2-46B5-801F-D4865D050E5A}.Release|x64.ActiveCfg = Release|Any CPU
		{F31B8A71-B5E2-46B5-801F-D4865D050E5A}.Release|x64.Build.0 = Release|Any CPU
		{F31B8A71-B5E2-46B5-801F-D4865D050E5A}.Release|x86.ActiveCfg = Release|Any CPU
		{F31B8A71-B5E2-46B5-801F-D4865D050E5A}.Release|x86.Build.0 = Release|Any CPU
		{DDF7496A-8437-423E-B702-DDF4D081796E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{DDF7496A-8437-423E-B702-DDF4D081796E}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{DDF7496A-8437-423E-B702-DDF4D081796E}.Debug|x64.ActiveCfg = Debug|Any CPU
		{DDF7496A-8437-423E-B702-DDF4D081796E}.Debug|x64.Build.0 = Debug|Any CPU
		{DDF7496A-8437-423E-B702-DDF4D081796E}.Debug|x86.ActiveCfg = Debug|Any CPU
		{DDF7496A-8437-423E-B702-DDF4D081796E}.Debug|x86.Build.0 = Debug|Any CPU
		{DDF7496A-8437-423E-B702-DDF4D081796E}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{DDF7496A-8437-423E-B702-DDF4D081796E}.Release|Any CPU.Build.0 = Release|Any CPU
		{DDF7496A-8437-423E-B702-DDF4D081796E}.Release|x64.ActiveCfg = Release|Any CPU
		{DDF7496A-8437-423E-B702-DDF4D081796E}.Release|x64.Build.0 = Release|Any CPU
		{DDF7496A-8437-423E-B702-DDF4D081796E}.Release|x86.ActiveCfg = Release|Any CPU
		{DDF7496A-8437-423E-B702-DDF4D081796E}.Release|x86.Build.0 = Release|Any CPU
	EndGlobalSection
	GlobalSection(SolutionProperties) = preSolution
		HideSolutionNode = FALSE
	EndGlobalSection
	GlobalSection(NestedProjects) = preSolution
		{30446FC8-83DA-47A7-ADE8-526A87E1AA63} = {2531614F-61A8-474C-8896-EF9A3719691B}
		{F31B8A71-B5E2-46B5-801F-D4865D050E5A} = {2531614F-61A8-474C-8896-EF9A3719691B}
		{DDF7496A-8437-423E-B702-DDF4D081796E} = {2531614F-61A8-474C-8896-EF9A3719691B}
	EndGlobalSection
	GlobalSection(ExtensibilityGlobals) = postSolution
		SolutionGuid = {A2A492F2-7DC1-4505-8CDB-7EBC8F90E9D2}
	EndGlobalSection
EndGlobal

And we now have:

<Solution>
  <Configurations>
    <Platform Name="Any CPU" />
    <Platform Name="x64" />
    <Platform Name="x86" />
  </Configurations>
  <Folder Name="/src/">
    <Project Path="src/JiraTicketAutomator/JiraTicketAutomator.csproj" />
    <Project Path="src/UpsertJiraTicket/UpsertJiraTicket.csproj" />
  </Folder>
</Solution>

And if you’re wondering, yes, I also deleted an old, now unnecessary project while cleaning things up 🙂

What About MSBuild Performance?

Switching from .sln to .slnx does not magically make msbuild.exe compile your code faster.

The new solution format mostly improves how solutions are described and parsed, while the underlying project graph and build targets that MSBuild executes stay the same.

You might see slightly quicker solution load or parsing times in very large repos, but the real wins are reduced merge conflicts and easier automation rather than raw build throughput.

Where things do get more interesting is with custom MSBuild tasks in the .NET 10 time frame. MSBuild running under the dotnet CLI has used a modern .NET runtime for a while now, but starting with .NET 10 the classic msbuild.exe and Visual Studio 2026 can also run tasks that are built for .NET instead of .NET Framework.

That means task authors can now write a single .NET-based task and have it work in Visual Studio, msbuild.exe, and dotnet build, which simplifies maintenance and lets those tasks benefit from the performance and API improvements in the latest runtime.

Bonus: A Few .NET 10 SDK Features Worth Knowing

While you play around with .slnx, it’s also worth being aware of a couple of SDK and tooling changes that arrive with .NET 10. First, .NET tools are getting much more powerful: a single tool package can now ship binaries for multiple platforms, including self-contained, trimmed, or even AOT-compiled variants, and the CLI will pick the right one at install or run time.

.NET 10 also formalizes temporary tool execution – without installing them – with the dotnet tool exec command, which is perfect for CI jobs or ephemeral scripts.

Second, there are some nice quality-of-life improvements for people who live in terminals and pipelines. The new --cli-schema switch emits a JSON description of any dotnet command, which is great for shell integration and script generators, and the CLI now supports noun-first aliases like dotnet package add alongside the classic verb-first forms.

On top of that, .NET 10 enables pruning of unused framework-provided package references and adds native completion scripts and easier container publishing, all of which help keep your builds a bit slimmer, and daily workflows more ergonomic.

What’s new in .NET 10 | Microsoft Learn

What’s new in the SDK and tooling for .NET 10 | Microsoft Learn

dotnet tool exec command – .NET CLI | Microsoft Learn

These Solutions are Engineered by Humans

Did you find this article interesting? Are you an “under the hood” kind of person? We’re really big on automation and we’re always looking for people in a similar vein to fill roles like this one as well as other roles here at Würth IT Italy.

Francesco Belacca

Francesco Belacca

Author

Francesco Belacca

Leave a Reply

Your email address will not be published. Required fields are marked *

Archive