|
| 1 | +# BuildNonexistentProjectsByDefault Global Property |
| 2 | + |
| 3 | +## Summary |
| 4 | + |
| 5 | +The `_BuildNonexistentProjectsByDefault` global property enables MSBuild tasks to build in-memory or virtual projects by defaulting to `SkipNonexistentProjects=Build` behavior when the property is not explicitly specified. |
| 6 | + |
| 7 | +## Background and Motivation |
| 8 | + |
| 9 | +### Problem |
| 10 | + |
| 11 | +[File-based applications][file-based-apps] (such as `dotnet run file.cs`) create in-memory MSBuild projects without corresponding physical `.csproj` files on disk. When these projects use common targets that include MSBuild tasks referencing the current project (e.g., `<MSBuild Projects="$(MSBuildProjectFullPath)" />`), the build fails because MSBuild cannot find the project file on disk, even though the project content is available in memory. |
| 12 | + |
| 13 | +This pattern is very common in .NET SDK targets, creating friction for file-based applications that need to reuse existing build logic. |
| 14 | + |
| 15 | +### Use Case Example |
| 16 | + |
| 17 | +Consider a file-based application that creates an in-memory project: |
| 18 | + |
| 19 | +```csharp |
| 20 | +var xmlReader = XmlReader.Create(new StringReader(projectText)); |
| 21 | +var projectRoot = ProjectRootElement.Create(xmlReader); |
| 22 | +projectRoot.FullPath = Path.Join(Environment.CurrentDirectory, "test.csproj"); |
| 23 | +// Project exists in memory but not on disk |
| 24 | +``` |
| 25 | + |
| 26 | +When this project uses targets containing: |
| 27 | +```xml |
| 28 | +<MSBuild Projects="$(MSBuildProjectFullPath)" Targets="SomeTarget" /> |
| 29 | +``` |
| 30 | + |
| 31 | +The build fails with: |
| 32 | +> MSB3202: The project file "test.csproj" was not found. |
| 33 | +
|
| 34 | +## Solution |
| 35 | + |
| 36 | +### The `_BuildNonexistentProjectsByDefault` Property |
| 37 | + |
| 38 | +This internal global property provides an opt-in mechanism to change the default behavior of MSBuild tasks when `SkipNonexistentProjects` is not explicitly specified. |
| 39 | + |
| 40 | +**Property Name:** `_BuildNonexistentProjectsByDefault` |
| 41 | +**Type:** Boolean |
| 42 | +**Default:** `false` (when not set) |
| 43 | +**Scope:** Global property only |
| 44 | + |
| 45 | +### Behavior |
| 46 | + |
| 47 | +When `_BuildNonexistentProjectsByDefault` is set to `true`: |
| 48 | + |
| 49 | +1. **MSBuild tasks** that don't explicitly specify `SkipNonexistentProjects` will default to `SkipNonexistentProjects="Build"` instead of `SkipNonexistentProjects="False"` |
| 50 | +2. **In-memory projects** with a valid `FullPath` can be built even when no physical file exists on disk |
| 51 | +3. **Existing explicit settings** are preserved - if `SkipNonexistentProjects` is explicitly set on the MSBuild task, that takes precedence |
| 52 | + |
| 53 | +### Implementation Details |
| 54 | + |
| 55 | +The property is checked in two MSBuild task implementations: |
| 56 | + |
| 57 | +1. **`src/Tasks/MSBuild.cs`** - The standard MSBuild task implementation |
| 58 | +2. **`src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/MSBuild.cs`** - The backend intrinsic task implementation |
| 59 | + |
| 60 | +The logic follows this precedence order: |
| 61 | + |
| 62 | +1. If `SkipNonexistentProjects` is explicitly set on the MSBuild task → use that value |
| 63 | +2. If `SkipNonexistentProjects` metadata is specified on the project item → use that value |
| 64 | +3. If `_BuildNonexistentProjectsByDefault=true` is set globally → default to `Build` |
| 65 | +4. Otherwise → default to `Error` (existing behavior) |
| 66 | + |
| 67 | +## Usage |
| 68 | + |
| 69 | +### File-based Applications |
| 70 | + |
| 71 | +File-based applications can set this property when building in-memory projects: |
| 72 | + |
| 73 | +```csharp |
| 74 | +var project = ObjectModelHelpers.CreateInMemoryProject(projectContent); |
| 75 | +project.SetGlobalProperty("_BuildNonexistentProjectsByDefault", "true"); |
| 76 | +bool result = project.Build(); |
| 77 | +``` |
| 78 | + |
| 79 | +### SDK Integration |
| 80 | + |
| 81 | +The .NET SDK will use this property to enable building file-based applications without workarounds when calling MSBuild tasks that reference the current project. |
| 82 | + |
| 83 | +## Breaking Changes |
| 84 | + |
| 85 | +**None.** This is an opt-in feature with an internal property name (prefixed with `_`). Existing behavior is preserved when the property is not set. |
| 86 | + |
| 87 | +## Alternatives Considered |
| 88 | + |
| 89 | +1. **Always allow building in-memory projects**: This would be a breaking change as it could mask legitimate errors when projects are missing. |
| 90 | + |
| 91 | +2. **Add a new MSBuild task parameter**: This would require modifying all existing targets to use the new parameter, creating compatibility issues. |
| 92 | + |
| 93 | +3. **Modify SkipNonexistentProjects default**: This would be a breaking change affecting all MSBuild usage. |
| 94 | + |
| 95 | +4. **Engine-level configuration**: More complex to implement and would require serialization across build nodes. |
| 96 | + |
| 97 | +The global property approach provides the needed functionality while maintaining backward compatibility and requiring minimal changes to the MSBuild task implementations. |
| 98 | + |
| 99 | +## Related Issues |
| 100 | + |
| 101 | +- [#12058](https://github.com/dotnet/msbuild/issues/12058) - MSBuild task should work on virtual projects |
| 102 | +- [dotnet/sdk#49745](https://github.com/dotnet/sdk/pull/49745) - Remove MSBuild hacks for virtual project building |
| 103 | +- [NuGet/Home#14148](https://github.com/NuGet/Home/issues/14148) - Related workaround requirements |
| 104 | +- [File-based app spec][file-based-apps] - Motivating use-case |
| 105 | + |
| 106 | +[file-based-apps]: https://github.com/dotnet/sdk/blob/main/documentation/general/dotnet-run-file.md |
0 commit comments