{"id":17182340,"url":"https://github.com/simoncropp/setstartupprojects","last_synced_at":"2026-05-20T02:09:03.651Z","repository":{"id":32151342,"uuid":"35724352","full_name":"SimonCropp/SetStartupProjects","owner":"SimonCropp","description":"Setting Visual Studio startup projects by hacking the suo","archived":false,"fork":false,"pushed_at":"2025-03-25T10:18:55.000Z","size":1628,"stargazers_count":46,"open_issues_count":2,"forks_count":8,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-28T21:06:54.703Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/SimonCropp.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"license.txt","code_of_conduct":"code_of_conduct.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"SimonCropp"}},"created_at":"2015-05-16T13:16:42.000Z","updated_at":"2025-03-25T10:16:21.000Z","dependencies_parsed_at":"2023-02-12T22:16:44.899Z","dependency_job_id":"ce4aefba-b128-4556-9c67-1fc483a7d581","html_url":"https://github.com/SimonCropp/SetStartupProjects","commit_stats":{"total_commits":942,"total_committers":9,"mean_commits":"104.66666666666667","dds":0.5318471337579618,"last_synced_commit":"eb985a759a6d757772fbf54ea237496b64474c78"},"previous_names":[],"tags_count":13,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SimonCropp%2FSetStartupProjects","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SimonCropp%2FSetStartupProjects/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SimonCropp%2FSetStartupProjects/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SimonCropp%2FSetStartupProjects/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/SimonCropp","download_url":"https://codeload.github.com/SimonCropp/SetStartupProjects/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247256112,"owners_count":20909240,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-15T00:36:46.395Z","updated_at":"2026-05-20T02:09:03.556Z","avatar_url":"https://github.com/SimonCropp.png","language":"C#","funding_links":["https://github.com/sponsors/SimonCropp"],"categories":[],"sub_categories":[],"readme":"\u003c!--\nGENERATED FILE - DO NOT EDIT\nThis file was generated by [MarkdownSnippets](https://github.com/SimonCropp/MarkdownSnippets).\nSource File: /readme.source.md\nTo change this file edit the source file and then run MarkdownSnippets.\n--\u003e\n\n# \u003cimg src=\"/src/icon.png\" height=\"30px\"\u003e SetStartupProjects\n\n[![Build status](https://ci.appveyor.com/api/projects/status/aqarlqoac6n3w6qd/branch/main?svg=true)](https://ci.appveyor.com/project/SimonCropp/SetStartupProjects)\n[![NuGet Status](https://img.shields.io/nuget/v/SetStartupProjects.svg)](https://www.nuget.org/packages/SetStartupProjects/)\n\nSetting Visual Studio startup projects by hacking the suo\n\n**See [Milestones](../../milestones?state=closed) for release notes.**\n\n\n## NuGet package\n\nhttps://nuget.org/packages/SetStartupProjects/\n\n\n## Usage\n\n\n### Passing in explicit Guids\n\nThe raw api makes no assumptions and takes an explicit list of project Guids.\n\n```csharp\nList\u003cstring\u003e startupProjectGuids = new()\n{\n    \"11111111-1111-1111-1111-111111111111\",\n    \"22222222-2222-2222-2222-222222222222\"\n};\nStartProjectSuoCreator startProjectSuoCreator = new();\nstartProjectSuoCreator.CreateForSolutionFile(solutionFilePath, startupProjectGuids);\n```\n\n### Use the build in convention\n\nBy 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.\n\nFor example if the solution \"TheSolution.sln\" contains two projects and you only want to start Project1 the content of TheSolution.StartupProjects.txt would be:\n\n```\nProject1\\Project1.csproj\n```\n\nAnd then can be used as follows:\n\n```csharp\nvar startupProjectGuids = new StartProjectFinder()\n    .GetStartProjects(solutionFilePath)\n    .ToList();\nStartProjectSuoCreator startProjectSuoCreator = new();\nstartProjectSuoCreator.CreateForSolutionFile(solutionFilePath, startupProjectGuids);\n```\n\n\n## Justification\n\nAs part of the a documentation site, **manipulating Visual Studio .suo files** allows control of start-up projects.\n\nMany 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:\n\n * Multiple start-up projects need to be documented, taking up space better used for the sample description.\n * People who download samples need manually setting the start-up projects.\n\nOften people forget to set start-up projects and one of two things happen:\n\n 1. If a Class Library is first Visual Studio gives a warning about \"A Class Library cannot be started directly\"\n 1. 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.\n\n\n### Why not commit the SUO to source control\n\nThe start-up projects for a solution are stored in the [Solution User Options (.Suo) File](https://msdn.microsoft.com/en-us/library/bb165909.aspx). 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.\n\n\n## Surely this is a solved problem\n\nVisual 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:\n\n* **Manipulate the order of projects in a solution**: The [Sln Startup Project](https://github.com/michaKFromParis/slnStartupProject) 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.\n* **Switch from within Visual Studio**: The [switchstartupproject](https://bitbucket.org/thirteen/switchstartupproject/wiki/Home) 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.\n\n\n## The underlying format of an suo\n\nA `.suo` is actually a \"OLE Compound Document File\" which seems to be synonymous with the [Microsoft Compound File Binary File (MCDF) Format](https://msdn.microsoft.com/en-us/library/dd942138.aspx). This is the same format used by previous generation Office documents (`.doc`, `.xls`, `.ppt`).\n\n\n## OpenMCDF\n\nThis project uses [Open MCDF](https://github.com/ironfede/openmcdf) ([nuget](https://www.nuget.org/packages/OpenMcdf/)) to manipulate the underlying binary structure of the `.suo`.\n\n\u003e 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.\n\nOpenMCDF ships with a sample Windows Forms application, for browsing and editing files, named \"Structured Storage Explorer\".\n\n\n## The underlying suo structure\n\nMCDF files have the concept of streams and, in an .suo file, the stream named `SolutionConfiguration` contains the start-up projects. \n\nTo view this structure taking a sample solution with 3 projects and hack the project GUIDs to make it easier to debug\n\n * `ClassLibrary` GUID=`99999999-9999-9999-9999-999999999999`\n * `ConsoleApplication1` GUID=`11111111-1111-1111-1111-111111111111`\n * `ConsoleApplication2` GUID=`22222222-2222-2222-2222-222222222222`\n\nSet the start-up projects to be `ConsoleApplication1` and `ConsoleApplication2`.\n\nOpening the `.suo` for this solution in Structured Storage Explorer and navigating to the `SolutionConfiguration` Stream will show\n\n![](src/Images/structuredstorageexplorer.png)\n\nYou will note the text contains the project GUIDs mentioned above.\n\n\n## The Encoding of the `SolutionConfiguration` Stream\n\nNote 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.\n\nThe 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:\n\n```csharp\nvar utf16 = Encoding.GetEncodings()\n    .Single(x =\u003e x.Name == \"utf-16\")\n    .GetEncoding();\nusing (var solutionStream = File.OpenRead(suoPath))\nusing (CompoundFile compoundFile = new(solutionStream, CFSUpdateMode.ReadOnly, CFSConfiguration.SectorRecycle | CFSConfiguration.EraseFreeSectors))\n{\n    var configStream = compoundFile.RootStorage.GetStream(\"SolutionConfiguration\");\n    var bytes = configStream.GetData();\n    Debug.WriteLine(utf16.GetString(bytes));\n}\n```\n\nWhich gives us this\n\n    MultiStartupProj = ;4 {99999999-9999-9999-9999-999999999999}...\n\nNote that Visual Studio has trouble rendering the characters. If you instead save the contents to a text file. \n\n```csharp\nusing (var solutionStream = File.OpenRead(suoPath))\nusing (CompoundFile compoundFile = new(solutionStream, CFSUpdateMode.ReadOnly, CFSConfiguration.SectorRecycle | CFSConfiguration.EraseFreeSectors))\n{\n    var configStream = compoundFile.RootStorage.GetStream(\"SolutionConfiguration\");\n    var bytes = configStream.GetData();\n\n    var utf16 = Encoding.GetEncodings()\n        .Single(x =\u003e x.Name == \"utf-16\")\n        .GetEncoding();\n    File.WriteAllText(\"temp.txt\", utf16.GetString(bytes), utf16);\n}\n```\n\nOpening `temp.txt` in [Sublime Text](http://www.sublimetext.com/) will reveal this (new lines added after `;` characters for clarity)\n\n![](src/Images/textoutput.png)\n\nNote the existence of [control characters](http://en.wikipedia.org/wiki/Control_character#In_ASCII) explains why both the MCDF explorer and Visual Studio had trouble rendering them.\n\n\n## Dissecting the contents\n\nThere are several other settings stored in the configuration stream. The important parts related to enabling multiple start projects are as follows:\n\n\n### The key indicating the start of the multiple startup section:\n\n![](src/Images/enablemultistart.png)\n\n\n### Defining each project that should be part of the multiple start:\n\n![](src/Images/startprojectformat.png)\n\n\n### Redundant information\n\nThe rest are configuration options and [miscellaneous project files](https://msdn.microsoft.com/en-us/library/bb166496.aspx) that Visual Studio would usually default to when there is no `.suo`.\n\n\n## Minimum settings to write back\n\nFor the sample solutions project GUIDs the minimum that needs to be written back to that stream is:\n\n![](src/Images/desiredoutput.png)\n\n\n## The suo template used\n\nAs 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**.\n\n\n## The MultiStartupProj writing code\n\nThe underlying code to write the startup GUIDs to the `.suo` is as follows:\n\n```csharp\nstatic void SetSolutionConfigValue(CFStream cfStream, IEnumerable\u003cstring\u003e startupProjectGuids)\n{\n    var single = Encoding.GetEncodings().Single(x =\u003e x.Name == \"utf-16\");\n    var encoding = single.GetEncoding();\n    var NUL = '\\u0000';\n    var DC1 = '\\u0011';\n    var ETX = '\\u0003';\n    var SOH = '\\u0001';\n\n    var builder = new StringBuilder();\n    builder.Append(DC1);\n    builder.Append(NUL);\n    builder.Append(\"MultiStartupProj\");\n    builder.Append(NUL);\n    builder.Append('=');\n    builder.Append(ETX);\n    builder.Append(SOH);\n    builder.Append(NUL);\n    builder.Append(';');\n    foreach (var startupProjectGuid in startupProjectGuids)\n    {\n        builder.Append('4');\n        builder.Append(NUL);\n        builder.AppendFormat(\"{{{0}}}.dwStartupOpt\", startupProjectGuid);\n        builder.Append(NUL);\n        builder.Append('=');\n        builder.Append(ETX);\n        builder.Append(DC1);\n        builder.Append(NUL);\n        builder.Append(';');\n    }\n\n    var newBytes = encoding.GetBytes(builder.ToString());\n    cfStream.SetData(newBytes);\n}\n```\n\n\n## Using the library on the Sample Solution\n\nUsing the SetStartupProjects nuget the startup projects for the Sample Solution can be written back using the following:\n\n```csharp\nList\u003cstring\u003e startupProjectGuids = new()\n{\n    \"11111111-1111-1111-1111-111111111111\",\n    \"22222222-2222-2222-2222-222222222222\"\n};\nStartProjectSuoCreator startProjectSuoCreator = new();\nstartProjectSuoCreator.CreateForSolutionDirectory(solutionDirectory, startupProjectGuids);\n```\n\n\n## Verifying the results\n\nOpening the Sample Solution you will note the startup projects have been changed.\n\n![](src/Images/itworked.png)\n\n\n## Multiple versions of Visual Studio\n\n`StartProjectSuoCreator` writes `.suo` files for Visual Studio 2017, 2019 and 2022.\n\n\n## Icon\n\n[Equestrian](ttps://thenounproject.com/term/equestrian/56632/) designed by [Gwyn Lewis](https://thenounproject.com/gwyn751@gmail.com/) from [The Noun Project](https://thenounproject.com).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsimoncropp%2Fsetstartupprojects","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsimoncropp%2Fsetstartupprojects","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsimoncropp%2Fsetstartupprojects/lists"}