With the release of Visual Studio 2017 and the RTM .NET Core tooling, the .NET command line has gone through a transformation. The project.json format is no more, and instead we have returned back to .csproj files. It's not your grand-daddy's .csproj however - the new .csproj is far leaner than previous MSBuild files, and massively reduces the reliance on GUIDs.

One of the biggest reasons for this is the need to make the files easily editable by hand. With .NET Core being cross platform, relying on Visual Studio to edit the files correctly with the magic GUIDs is no longer acceptable.

As well as a switch from project.json to csproj, the global.json file is no more - instead we're back to .sln files. these are primarily for when you're working with Visual Studio, and they're not entirely necessary for building .NET Core applications. In some cases though, if you're working in a cross-platform environment, you may need to edit sln files on mac/Linux.

Unfortunately, GUIDs in .sln files have survived the great .NET Core purge of 2017, so editing the files by hand isn't particularly fun. For example, the following .sln file contains two projects - a source code project and a test project:

Microsoft Visual Studio Solution File, Format Version 12.00  
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0  
MinimumVisualStudioVersion = 15.0.26124.0  
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{2C14C847-9839-4C69-A5A0-C95D64DAECF2}"  
EndProject  
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliApp", "src\CliApp\CliApp.csproj", "{D4DDD205-C160-4179-B8CF-B98E5066A187}"  
EndProject  
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{08F7408C-CA01-4495-A30C-F16F3FCBFDF2}"  
EndProject  
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliAppTests", "test\CliAppTests\CliAppTests.csproj", "{4283F8CC-9575-48E5-AD4C-B628DB5D6301}"  
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(SolutionProperties) = preSolution
        HideSolutionNode = FALSE
    EndGlobalSection
    GlobalSection(ProjectConfigurationPlatforms) = postSolution
        {D4DDD205-C160-4179-B8CF-B98E5066A187}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
        {D4DDD205-C160-4179-B8CF-B98E5066A187}.Debug|Any CPU.Build.0 = Debug|Any CPU
        {D4DDD205-C160-4179-B8CF-B98E5066A187}.Debug|x64.ActiveCfg = Debug|x64
        {D4DDD205-C160-4179-B8CF-B98E5066A187}.Debug|x64.Build.0 = Debug|x64
        {D4DDD205-C160-4179-B8CF-B98E5066A187}.Debug|x86.ActiveCfg = Debug|x86
        {D4DDD205-C160-4179-B8CF-B98E5066A187}.Debug|x86.Build.0 = Debug|x86
        {D4DDD205-C160-4179-B8CF-B98E5066A187}.Release|Any CPU.ActiveCfg = Release|Any CPU
        {D4DDD205-C160-4179-B8CF-B98E5066A187}.Release|Any CPU.Build.0 = Release|Any CPU
        {D4DDD205-C160-4179-B8CF-B98E5066A187}.Release|x64.ActiveCfg = Release|x64
        {D4DDD205-C160-4179-B8CF-B98E5066A187}.Release|x64.Build.0 = Release|x64
        {D4DDD205-C160-4179-B8CF-B98E5066A187}.Release|x86.ActiveCfg = Release|x86
        {D4DDD205-C160-4179-B8CF-B98E5066A187}.Release|x86.Build.0 = Release|x86
        {4283F8CC-9575-48E5-AD4C-B628DB5D6301}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
        {4283F8CC-9575-48E5-AD4C-B628DB5D6301}.Debug|Any CPU.Build.0 = Debug|Any CPU
        {4283F8CC-9575-48E5-AD4C-B628DB5D6301}.Debug|x64.ActiveCfg = Debug|x64
        {4283F8CC-9575-48E5-AD4C-B628DB5D6301}.Debug|x64.Build.0 = Debug|x64
        {4283F8CC-9575-48E5-AD4C-B628DB5D6301}.Debug|x86.ActiveCfg = Debug|x86
        {4283F8CC-9575-48E5-AD4C-B628DB5D6301}.Debug|x86.Build.0 = Debug|x86
        {4283F8CC-9575-48E5-AD4C-B628DB5D6301}.Release|Any CPU.ActiveCfg = Release|Any CPU
        {4283F8CC-9575-48E5-AD4C-B628DB5D6301}.Release|Any CPU.Build.0 = Release|Any CPU
        {4283F8CC-9575-48E5-AD4C-B628DB5D6301}.Release|x64.ActiveCfg = Release|x64
        {4283F8CC-9575-48E5-AD4C-B628DB5D6301}.Release|x64.Build.0 = Release|x64
        {4283F8CC-9575-48E5-AD4C-B628DB5D6301}.Release|x86.ActiveCfg = Release|x86
        {4283F8CC-9575-48E5-AD4C-B628DB5D6301}.Release|x86.Build.0 = Release|x86
    EndGlobalSection
    GlobalSection(NestedProjects) = preSolution
        {D4DDD205-C160-4179-B8CF-B98E5066A187} = {2C14C847-9839-4C69-A5A0-C95D64DAECF2}
        {4283F8CC-9575-48E5-AD4C-B628DB5D6301} = {08F7408C-CA01-4495-A30C-F16F3FCBFDF2}
    EndGlobalSection
EndGlobal

A bit overwhelming right? Luckily, the .NET Core command line provides a number of commands for creating and editing these files, so you don't have to dive into them with a text editor directly.

Creating a new solution file

In this example, I'll assume you've already created a couple of projects. You can use dotnet new to achieve this, whether you're creating a command line app, web app, library or test project.

You can also create your own dotnet new templates using new experimental features, that should be available in stable form for .NET Core 2.0. You can read about these features here.

You can create a new solution file in the current directory using:

dotnet new sln

You can also provide an optional name for the .sln file using --name filename, otherwise it will have the same name as the current folder.

$ dotnet new sln --name test
Content generation time: 20.8484 ms  
The template "Solution File" created successfully.  

This will create a new .sln file in the current folder. The solution file currently doesn't have any associated projects, but defines a number of build configurations. The command above creates the following file:

Microsoft Visual Studio Solution File, Format Version 12.00  
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0  
MinimumVisualStudioVersion = 15.0.26124.0  
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(SolutionProperties) = preSolution
        HideSolutionNode = FALSE
    EndGlobalSection
EndGlobal  

Adding a project to a solution file

Once you have a solution file, you can add a project to it using the sln add command, and provide the path to the project's .csproj file. This will add the project to an existing solution file in the current folder. The path to the project can be absolute or relative, but it will be added as a relative path in the .sln file.

dotnet sln add <path-to-project.csproj>

For example, to add a project located at src/CliApp/CliApp.csproj, when you have a single solution file in your current directory, you can use the following:

$ dotnet sln add "src\CliApp\CliApp.csproj"
Project `src\CliApp\CliApp.csproj` added to the solution.  

After running this, you'll see your project has been added to the .sln file, along with a src solution folder:

Microsoft Visual Studio Solution File, Format Version 12.00  
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0  
MinimumVisualStudioVersion = 15.0.26124.0  
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{FFEC406A-FBFB-4737-8C32-1CF34FAF2D6F}"  
EndProject  
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CliApp", "src\CliApp\CliApp.csproj", "{92B636D5-2C14-4445-B8C1-BBF93A03FA5D}"  
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(SolutionProperties) = preSolution
        HideSolutionNode = FALSE
    EndGlobalSection
    GlobalSection(ProjectConfigurationPlatforms) = postSolution
        {92B636D5-2C14-4445-B8C1-BBF93A03FA5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
        {92B636D5-2C14-4445-B8C1-BBF93A03FA5D}.Debug|Any CPU.Build.0 = Debug|Any CPU
        {92B636D5-2C14-4445-B8C1-BBF93A03FA5D}.Debug|x64.ActiveCfg = Debug|x64
        {92B636D5-2C14-4445-B8C1-BBF93A03FA5D}.Debug|x64.Build.0 = Debug|x64
        {92B636D5-2C14-4445-B8C1-BBF93A03FA5D}.Debug|x86.ActiveCfg = Debug|x86
        {92B636D5-2C14-4445-B8C1-BBF93A03FA5D}.Debug|x86.Build.0 = Debug|x86
        {92B636D5-2C14-4445-B8C1-BBF93A03FA5D}.Release|Any CPU.ActiveCfg = Release|Any CPU
        {92B636D5-2C14-4445-B8C1-BBF93A03FA5D}.Release|Any CPU.Build.0 = Release|Any CPU
        {92B636D5-2C14-4445-B8C1-BBF93A03FA5D}.Release|x64.ActiveCfg = Release|x64
        {92B636D5-2C14-4445-B8C1-BBF93A03FA5D}.Release|x64.Build.0 = Release|x64
        {92B636D5-2C14-4445-B8C1-BBF93A03FA5D}.Release|x86.ActiveCfg = Release|x86
        {92B636D5-2C14-4445-B8C1-BBF93A03FA5D}.Release|x86.Build.0 = Release|x86
    EndGlobalSection
    GlobalSection(NestedProjects) = preSolution
        {92B636D5-2C14-4445-B8C1-BBF93A03FA5D} = {FFEC406A-FBFB-4737-8C32-1CF34FAF2D6F}
    EndGlobalSection
EndGlobal  

Adding a project to a specific solution file

If you have multiple solution files in the current directory, then trying to run the previous command will give you an error similar to the following:

$ dotnet sln add "test\CliAppTests\CliAppTests.csproj"
Found more than one solution file in C:\Users\Sock\Repos\andrewlock\example\. Please specify which one to use.  

Instead, you must specify the name of the solution you wish to amend, by placing the path to the solution after sln:

dotnet sln <path-to-solution.sln> add <path-to-project.csproj>

For example,

dotnet sln "example.sln" add "test\CliAppTests\CliAppTests.csproj"

Note, when I first ran this command, I incorrectly placed the add parameter before the solution name, using dotnet sln add <solution> <project>. Unfortunately, this currently gives you a slightly confusing error (tracked here): Unhandled Exception: Microsoft.Build.Exceptions.InvalidProjectFileException: The project file could not be loaded. Data at the root level is invalid. Line 2, position 1.

Removing a project from a solution file

Removing a project from your solution is the mirror of adding a project - just be aware that, as before, you need to use the path to the .csproj file rather than just the name of the project folder, and if you have multiple .sln files in the current folder then you need to specify which one to modify

dotnet sln remove <path-to-project.csproj>

or

dotnet sln <path-to-solution.sln> remove <path-to-project.csproj>

This will remove the specified project, along with any associated sub folder nodes

$ dotnet sln remove src/CliApp/CliApp.csproj
Project reference `src\CliApp\CliApp.csproj` removed.  

Listing the projects in a solution file

The final command exposed by the .NET CLI is the ability to list the projects in the solution file, instead of having to open it up and wade through the litany of GUIDs:

dotnet sln list

Note that this command not only lists the project names (the paths to the .csproj files), it also lists the sub folders in which they reside. For example, the following solution contains two projects, one inside the src folder, one inside the test folder:

Solution folder

Listing the projects in this solution gives the following:

$ dotnet sln list
Project reference(s)  
--------------------
test  
test\CliAppTests\CliAppTests.csproj  
src  
src\CliApp\CliApp.csproj  

Summary

With .NET Core, cross platform development is a genuine first class citizen. With the .NET CLI, you can now manage your .sln files without needing to use Visual Studio or to mess with GUIDs in a text editor. This lets you create, add, remove and list projects. To see all the options available to you, run dotnet sln --help