Setting Visual Studio startup projects by hacking the suo
See Milestones for release notes.
https://nuget.org/packages/SetStartupProjects/
The raw api makes no assumptions and takes an explicit list of project Guids.
List<string> startupProjectGuids = new()
{
"11111111-1111-1111-1111-111111111111",
"22222222-2222-2222-2222-222222222222"
};
StartProjectSuoCreator startProjectSuoCreator = new();
startProjectSuoCreator.CreateForSolutionFile(solutionFilePath, startupProjectGuids);
By default startable projects are detected though interrogating the project files, i.e. all projects that are considered startable will be added to the startable list. To override this convention and hard code the list of startup projects add a file named {SolutionName}.StartupProjects.txt
in the same directory as the solution file. It should contain the relative paths to the project files you would like to use for startup projects.
For example if the solution "TheSolution.sln" contains two projects and you only want to start Project1 the content of TheSolution.StartupProjects.txt would be:
Project1\Project1.csproj
And then can be used as follows:
var startupProjectGuids = new StartProjectFinder()
.GetStartProjects(solutionFilePath)
.ToList();
StartProjectSuoCreator startProjectSuoCreator = new();
startProjectSuoCreator.CreateForSolutionFile(solutionFilePath, startupProjectGuids);
As part of the a documentation site, manipulating Visual Studio .suo files allows control of start-up projects.
Many of the samples have multiple "startable" components, eg services and websites that interact. To run correctly all these components have to run when the solution is "started". The default behavior of Visual Studio is to "set the first project in the solution as the start project". This is problematic since it results in several friction points:
- Multiple start-up projects need to be documented, taking up space better used for the sample description.
- People who download samples need manually setting the start-up projects.
Often people forget to set start-up projects and one of two things happen:
- If a Class Library is first Visual Studio gives a warning about "A Class Library cannot be started directly"
- If startable project is first it will start, but fails to start the other projects in the solution, resulting in the sample failing to run as expected.
The start-up projects for a solution are stored in the Solution User Options (.Suo) File. It is possible to modify the start-up projects, save the solution (and hence .suo
), and the commit that .suo
to source control. The problem is that the .suo
stores many other user preferences and, since it is binary, it is not possible to "only commit the start-up projects". This adds friction to the people maintain the samples since they need to be very careful about changes to the .suo
and the effect those changes have on downstream consumers.
Visual Studio was released in 1995 and with it the concept of an .suo
configuration file. Generally when a technology has been around for 20 years most of the problems are solved. If not there are enough nuggets of information around to piece together a solution. This does not seem to be the case for setting start project using code. There are several other approaches which do not match our requirements:
- Manipulate the order of projects in a solution: The Sln Startup Project can change the start-up projects by leveraging the side effect of Visual Studio starting the first project. It works by reordering the projects in the
.sln
file. However this approach only works for a single start-up project. - Switch from within Visual Studio: The switchstartupproject is a Visual Studio extension that supports multiple combinations of startup projects. Very useful but not an option since the context of this problem is not inside Visual Studio.
A .suo
is actually a "OLE Compound Document File" which seems to be synonymous with the Microsoft Compound File Binary File (MCDF) Format. This is the same format used by previous generation Office documents (.doc
, .xls
, .ppt
).
This project uses Open MCDF (nuget) to manipulate the underlying binary structure of the .suo
.
OpenMCDF is a 100% managed .net component that allows client applications to manipulate COM structured storage files, also known as Microsoft Compound Document Format files.
OpenMCDF ships with a sample Windows Forms application, for browsing and editing files, named "Structured Storage Explorer".
MCDF files have the concept of streams and, in an .suo file, the stream named SolutionConfiguration
contains the start-up projects.
To view this structure taking a sample solution with 3 projects and hack the project GUIDs to make it easier to debug
ClassLibrary
GUID=99999999-9999-9999-9999-999999999999
ConsoleApplication1
GUID=11111111-1111-1111-1111-111111111111
ConsoleApplication2
GUID=22222222-2222-2222-2222-222222222222
Set the start-up projects to be ConsoleApplication1
and ConsoleApplication2
.
Opening the .suo
for this solution in Structured Storage Explorer and navigating to the SolutionConfiguration
Stream will show
You will note the text contains the project GUIDs mentioned above.
Note that the "Structured Storage Explorer" has trouble decoding the binary value of the stream. This is due it making an incorrect assumption on the encoding.
The encoding of the SolutionConfiguration
Stream is Utf16, although this was only discovered by attempting multiple different encodings. Also from looking at other streams the choice of encoding does not seem to be consistent across all streams. You can read the value of the SolutionConfiguration
Stream using the OpenMCDF library as follows:
var utf16 = Encoding.GetEncodings()
.Single(x => x.Name == "utf-16")
.GetEncoding();
using (var solutionStream = File.OpenRead(suoPath))
using (CompoundFile compoundFile = new(solutionStream, CFSUpdateMode.ReadOnly, CFSConfiguration.SectorRecycle | CFSConfiguration.EraseFreeSectors))
{
var configStream = compoundFile.RootStorage.GetStream("SolutionConfiguration");
var bytes = configStream.GetData();
Debug.WriteLine(utf16.GetString(bytes));
}
Which gives us this
MultiStartupProj = ;4 {99999999-9999-9999-9999-999999999999}...
Note that Visual Studio has trouble rendering the characters. If you instead save the contents to a text file.
using (var solutionStream = File.OpenRead(suoPath))
using (CompoundFile compoundFile = new(solutionStream, CFSUpdateMode.ReadOnly, CFSConfiguration.SectorRecycle | CFSConfiguration.EraseFreeSectors))
{
var configStream = compoundFile.RootStorage.GetStream("SolutionConfiguration");
var bytes = configStream.GetData();
var utf16 = Encoding.GetEncodings()
.Single(x => x.Name == "utf-16")
.GetEncoding();
File.WriteAllText("temp.txt", utf16.GetString(bytes), utf16);
}
Opening temp.txt
in Sublime Text will reveal this (new lines added after ;
characters for clarity)
Note the existence of control characters explains why both the MCDF explorer and Visual Studio had trouble rendering them.
There are several other settings stored in the configuration stream. The important parts related to enabling multiple start projects are as follows:
The rest are configuration options and miscellaneous project files that Visual Studio would usually default to when there is no .suo
.
For the sample solutions project GUIDs the minimum that needs to be written back to that stream is:
As to include minimum baggage (extra .suo
settings) this project uses a template '.suo' taken from an empty project. This was created using a new empty solution with no projects and save the solution to produce an, almost empty, .suo
file SampleSolution.v12.suo
. Note that in this use case the target .suo
is replaced and not modified.
The underlying code to write the startup GUIDs to the .suo
is as follows:
static void SetSolutionConfigValue(CFStream cfStream, IEnumerable<string> startupProjectGuids)
{
var single = Encoding.GetEncodings().Single(x => x.Name == "utf-16");
var encoding = single.GetEncoding();
var NUL = '\u0000';
var DC1 = '\u0011';
var ETX = '\u0003';
var SOH = '\u0001';
var builder = new StringBuilder();
builder.Append(DC1);
builder.Append(NUL);
builder.Append("MultiStartupProj");
builder.Append(NUL);
builder.Append('=');
builder.Append(ETX);
builder.Append(SOH);
builder.Append(NUL);
builder.Append(';');
foreach (var startupProjectGuid in startupProjectGuids)
{
builder.Append('4');
builder.Append(NUL);
builder.AppendFormat("{{{0}}}.dwStartupOpt", startupProjectGuid);
builder.Append(NUL);
builder.Append('=');
builder.Append(ETX);
builder.Append(DC1);
builder.Append(NUL);
builder.Append(';');
}
var newBytes = encoding.GetBytes(builder.ToString());
cfStream.SetData(newBytes);
}
Using the SetStartupProjects nuget the startup projects for the Sample Solution can be written back using the following:
List<string> startupProjectGuids = new()
{
"11111111-1111-1111-1111-111111111111",
"22222222-2222-2222-2222-222222222222"
};
StartProjectSuoCreator startProjectSuoCreator = new();
startProjectSuoCreator.CreateForSolutionDirectory(solutionDirectory, startupProjectGuids);
Opening the Sample Solution you will note the startup projects have been changed.
StartProjectSuoCreator
writes .suo
files for Visual Studio 2017, 2019 and 2022.
Equestrian designed by Gwyn Lewis from The Noun Project.