Before getting into the format itself, here is a quick refresher on what a solution and a project both are:
A solution is a container for projects. A project is a container for files - in C# projects, those are mostly .cs source files. The solution file sits above everything and tells your tooling which projects exist and how they relate.
The old .sln format
The .sln format has been around since the early days of .NET Framework. It works, but it’s not very readable and it is difficult to maintain:
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36811.4
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WeatherApp.Api", "src\WeatherApp.Api\WeatherApp.Api.csproj", "{99AFF2EC-71B4-4951-A834-C124318A5BD7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WeatherApp.Core", "src\WeatherApp.Core\WeatherApp.Core.csproj", "{C2CC1C4F-A5F4-431A-A527-BA3C775D231F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WeatherApp.Api.Tests", "tests\WeatherApp.Api.Tests\WeatherApp.Api.Tests.csproj", "{6BC03B23-2942-47CA-8274-F6D15AFE6A66}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WeatherApp.Core.Tests", "tests\WeatherApp.Core.Tests\WeatherApp.Core.Tests.csproj", "{9351F873-D12E-4964-9146-46EB57BF508C}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{2081FAD6-6AA2-42C3-97E4-8BB0662A933F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{99AFF2EC-71B4-4951-A834-C124318A5BD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{99AFF2EC-71B4-4951-A834-C124318A5BD7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{99AFF2EC-71B4-4951-A834-C124318A5BD7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{99AFF2EC-71B4-4951-A834-C124318A5BD7}.Release|Any CPU.Build.0 = Release|Any CPU
{C2CC1C4F-A5F4-431A-A527-BA3C775D231F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C2CC1C4F-A5F4-431A-A527-BA3C775D231F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C2CC1C4F-A5F4-431A-A527-BA3C775D231F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C2CC1C4F-A5F4-431A-A527-BA3C775D231F}.Release|Any CPU.Build.0 = Release|Any CPU
{6BC03B23-2942-47CA-8274-F6D15AFE6A66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6BC03B23-2942-47CA-8274-F6D15AFE6A66}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6BC03B23-2942-47CA-8274-F6D15AFE6A66}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6BC03B23-2942-47CA-8274-F6D15AFE6A66}.Release|Any CPU.Build.0 = Release|Any CPU
{9351F873-D12E-4964-9146-46EB57BF508C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9351F873-D12E-4964-9146-46EB57BF508C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9351F873-D12E-4964-9146-46EB57BF508C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9351F873-D12E-4964-9146-46EB57BF508C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{99AFF2EC-71B4-4951-A834-C124318A5BD7} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{C2CC1C4F-A5F4-431A-A527-BA3C775D231F} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{6BC03B23-2942-47CA-8274-F6D15AFE6A66} = {2081FAD6-6AA2-42C3-97E4-8BB0662A933F}
{9351F873-D12E-4964-9146-46EB57BF508C} = {2081FAD6-6AA2-42C3-97E4-8BB0662A933F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {40A731E0-5CF1-4FC4-9584-5E9CECAD0755}
EndGlobalSection
EndGlobal
The GUIDs add so much noise along with the repeated configuration blocks. If you’re looking at this file, it takes so much mental bandwidth to understand which projects are in the solution (which is the main point of the solution as mentioned above).
You have to read past a lot of irrelevant details. Also, this is just a small multi-project solution, but in real-world enterprise-grade applications this grows much faster. And I am sure you can already image how that would look. What makes this entire thing even more difficult to deal with are merge conflicts, because every added project touches multiple sections.
The new .slnx format
.slnx was introduced in .NET 9. It uses standard XML and drops the GUIDs and all the verbose configuration data that’s rarely needed. Here is how the same solution looks with the new .slnx format:
<Solution>
<Folder Name="/src/">
<Project Path="src/WeatherApp.Api/WeatherApp.Api.csproj" />
<Project Path="src/WeatherApp.Core/WeatherApp.Core.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/WeatherApp.Api.Tests/WeatherApp.Api.Tests.csproj" />
<Project Path="tests/WeatherApp.Core.Tests/WeatherApp.Core.Tests.csproj" />
</Folder>
</Solution>
Yeap! That’s it. You can immediately see the structure. You have two folders - src and tests and within those you simply see which projects exist. No more GUIDs or configuration sections. And since it’s standard XML, merge conflicts are so much more straightforward!
Note that project files stay as .csproj. .slnx just makes solution files use the same clean XML format that project files have always used.
It is important to note that starting with .NET 10, new solutions are generated in .slnx format by default. And for any existing .sln files, migration is a one-step operation!
Migrating via the .NET CLI
Run the following from inside your solution directory:
dotnet sln migrate
If there’s only one .sln file in the directory, you can omit the filename, but if you have multiple .sln files you can simply pass the path like so:
dotnet sln migrate YourSolution.sln
This creates the new .slnx file alongside the existing .sln. Once you’ve verified things work, simply delete the old file and you are ready to go.
Migrating via Visual Studio
Open the .sln file in Visual Studio, then go to File → Save As, select XML Solution File (.slnx) from the file type dropdown, and save. The new .slnx solution file should now be created in the same location.
After migrating
Regardless of which path you take, it is always a good idea to check that everything still builds and all tests pass:
dotnet build
dotnet test
Both commands will use the .slnx file automatically once the .sln is removed. If everything builds and all tests pass, then the migration is complete. Congrats!
The takeaway
.slnx is a straightforward improvement. You have the same core information, but far less noise and a standard XML format, consistent with the .csproj format also easier when it comes to merge conflicts. Migration is a single command, but if you’re starting a new project on .NET 10 or later, you don’t even have to worry about migration since you get it automatically. This is definitely a meaningful quality of life win for every .NET developer.