Implement #:ref directive for file-based apps#53480
Implement #:ref directive for file-based apps#53480jjonescz merged 21 commits intodotnet:release/10.0.3xxfrom
#:ref directive for file-based apps#53480Conversation
There was a problem hiding this comment.
Pull request overview
Adds support for #:ref in file-based apps so a .cs file can reference another file-based .cs as a compiled library (via virtual MSBuild projects), including conversion behavior and documentation.
Changes:
- Introduce
CSharpDirective.Refparsing, validation/feature-flag gating, path expansion/resolution, and project-file emission via injected<ProjectReference>. - Update
dotnet runvirtual project construction to pre-create/register referenced virtual projects, and update caching decisions to avoid#:refscenarios. - Extend
dotnet project convertand tests/docs/resources to support converting#:refgraphs into sibling library projects with appropriate project references.
Reviewed changes
Copilot reviewed 36 out of 36 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| test/dotnet.Tests/CommandTests/Run/RunFileTests.cs | Adds end-to-end tests for #:ref (path handling, transitive refs, feature flag gating, and optimization interactions). |
| test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs | Adds conversion tests validating #:ref becomes project references and detects output folder conflicts. |
| src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs | Adds OutputType configurability, evaluates #:ref, gates it via feature flag, and emits ProjectReference entries for refs. |
| src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs | Disables cache save/compute for #:ref and recursively creates referenced virtual projects so MSBuild can resolve them. |
| src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs | Implements recursive conversion of #:ref files into library projects and validates duplicate target directories. |
| src/Cli/dotnet/Commands/CliCommandStrings.resx | Adds new CLI string for duplicate ref folder-name conversion error. |
| src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf | Localizes the new duplicate-folder conversion error string. |
| src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf | Localizes the new duplicate-folder conversion error string. |
| src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf | Localizes the new duplicate-folder conversion error string. |
| src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf | Localizes the new duplicate-folder conversion error string. |
| src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf | Localizes the new duplicate-folder conversion error string. |
| src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf | Localizes the new duplicate-folder conversion error string. |
| src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf | Localizes the new duplicate-folder conversion error string. |
| src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf | Localizes the new duplicate-folder conversion error string. |
| src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf | Localizes the new duplicate-folder conversion error string. |
| src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf | Localizes the new duplicate-folder conversion error string. |
| src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf | Localizes the new duplicate-folder conversion error string. |
| src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf | Localizes the new duplicate-folder conversion error string. |
| src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf | Localizes the new duplicate-folder conversion error string. |
| src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs | Implements the #:ref directive model, parsing, and file-path resolution/validation. |
| src/Cli/Microsoft.DotNet.FileBasedPrograms/FileBasedProgramsResources.resx | Adds #:ref-specific error strings (invalid directive, missing file). |
| src/Cli/Microsoft.DotNet.FileBasedPrograms/InternalAPI.Unshipped.txt | Updates internal API surface to include the new Ref directive types/consts. |
| src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hant.xlf | Localizes new #:ref-related error strings. |
| src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hans.xlf | Localizes new #:ref-related error strings. |
| src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.tr.xlf | Localizes new #:ref-related error strings. |
| src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ru.xlf | Localizes new #:ref-related error strings. |
| src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pt-BR.xlf | Localizes new #:ref-related error strings. |
| src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pl.xlf | Localizes new #:ref-related error strings. |
| src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ko.xlf | Localizes new #:ref-related error strings. |
| src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ja.xlf | Localizes new #:ref-related error strings. |
| src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.it.xlf | Localizes new #:ref-related error strings. |
| src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.fr.xlf | Localizes new #:ref-related error strings. |
| src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.es.xlf | Localizes new #:ref-related error strings. |
| src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.de.xlf | Localizes new #:ref-related error strings. |
| src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.cs.xlf | Localizes new #:ref-related error strings. |
| documentation/general/dotnet-run-file.md | Documents #:ref behavior, path resolution rules, feature flag, and grow-up/conversion behavior. |
|
@333fred @RikkiGibson @MiYanni for reviews, thanks |
| Unlike `#:project`, `#:ref` points to a `.cs` file (not a `.csproj` file or directory). | ||
|
|
||
| The referenced file is itself a file-based program with its own virtual project (defaulting to `OutputType=Exe`). | ||
| Library files without an entry point should use `#:property OutputType=Library` to avoid compilation errors. |
There was a problem hiding this comment.
While a "library FBA" might not have an "entry point", I think it should have a "main file".
We should be able to look at a file in isolation and realize it is specifically something which would be valid to reference via #:ref.
Otherwise, the editor is going to have to identify the closure of projects to load, by actually going ahead and loading every FBA it is able to discover, then examining all the targets of #:refs within all those FBAs, and loading those recursively until we reach a stable state. I think this will be more costly to implement on the editor side.
Also, I think this ambiguity would end up being passed on to users, who may struggled to determine whether something is the "main file" of a file-based library, when the file doesn't have a clear marker indicating that's what it is, rather than being some helper file which is meant to be #:included in something.
I would honestly consider requiring that "files referenced by #:ref, start with either #! (to indicate they're also executable), or for example #:library (to say they're just libraries), and using that to ensure that "automatic discovery" finds them. This should make it so that the editor "just works" with such reference graphs.
There was a problem hiding this comment.
What if we require they start with either #! or have #:property OutputType=Library somewhere among the directives of the entry point file?
There was a problem hiding this comment.
have
#:property OutputType=Librarysomewhere among the directives of the entry point file?
I lean a bit against this as it seems surprising for the directive to have different semantics here than in a props file, and for this property name in particular to have this effect.
I think while we are behind an experimental flag, we can ship command line support without a "main file marker". I think we'll have to have a separate conversation about how to cost/schedule editor support also, and possibly make a change to add/require this marker later.
Does that seem reasonable? Adding @phil-allen-msft for visibility.
There was a problem hiding this comment.
Yes, I think the core dotnet CLI part of the #:ref is given (it's just a way to specify <ProjectReference Include="file.cs"/>), so no reason to block that. But we may need some add-ons to improve the IDE experience (since we don't have solution files like normal projects do) - but that feels independent and we have the feature flag to avoid breaking changes.
| Unlike `#:project`, `#:ref` points to a `.cs` file (not a `.csproj` file or directory). | ||
|
|
||
| The referenced file is itself a file-based program with its own virtual project (defaulting to `OutputType=Exe`). | ||
| Library files without an entry point should use `#:property OutputType=Library` to avoid compilation errors. |
There was a problem hiding this comment.
have
#:property OutputType=Librarysomewhere among the directives of the entry point file?
I lean a bit against this as it seems surprising for the directive to have different semantics here than in a props file, and for this property name in particular to have this effect.
I think while we are behind an experimental flag, we can ship command line support without a "main file marker". I think we'll have to have a separate conversation about how to cost/schedule editor support also, and possibly make a change to add/require this marker later.
Does that seem reasonable? Adding @phil-allen-msft for visibility.
|
@333fred for another review, thanks |
|
@333fred for another review, thanks |
| // Pre-validate ref target directories (check for duplicates and existing dirs). | ||
| if (hasRefs) | ||
| { | ||
| var usedFolderNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { entryPointName }; |
There was a problem hiding this comment.
What happens on Linux when I do:
#:ref Lib.cs
#:ref lib.csThere was a problem hiding this comment.
During dotnet run, you get an error for a duplicate directive (that's not limited to #:ref, you'd get it for #:project too). That should go away with #51139 (comment) (I'm planning to open a PR for that once this one is merged). Then it will just be allowed, and we will let MSBuild deal with it.
During dotnet project convert, you will always get an error for this case. It's hard to tell whether the file system is case-sensitive though. Starting like this seems better (potentially disallowing valid cases rather than allowing invalid cases), we can revisit in the future.
| continue; | ||
| } | ||
|
|
||
| var refPath = refDirective.ResolvedPath ?? Path.GetFullPath(Path.Combine(sourceDirectory, refDirective.Name.Replace('\\', '/'))); |
There was a problem hiding this comment.
What happens in the presence of symlinks?
There was a problem hiding this comment.
Like if you have #:ref lib_link.cs where lib_link.cs is a symlink to lib.cs? Since we are not actively resolving symlinks, the conversion would simply create lib_link.csproj and lib_link.cs (and delete the original lib_link.cs file if you specify to delete sources). Basically it should be transparent as expected - as if the symlinked file was a copy of the original.
Resolves #53478.
Needs dotnet/msbuild#13389.