Skip to content

Implement #:ref directive for file-based apps#53480

Merged
jjonescz merged 21 commits intodotnet:release/10.0.3xxfrom
jjonescz:52532-ref-directive-2
Apr 15, 2026
Merged

Implement #:ref directive for file-based apps#53480
jjonescz merged 21 commits intodotnet:release/10.0.3xxfrom
jjonescz:52532-ref-directive-2

Conversation

@jjonescz
Copy link
Copy Markdown
Member

@jjonescz jjonescz commented Mar 16, 2026

Resolves #53478.
Needs dotnet/msbuild#13389.

@jjonescz jjonescz added the Area-run-file Items related to the "dotnet run <file>" effort label Mar 16, 2026
@jjonescz jjonescz changed the base branch from main to release/10.0.3xx March 16, 2026 13:05
@jjonescz jjonescz marked this pull request as ready for review March 26, 2026 13:57
@jjonescz jjonescz requested review from a team and Copilot March 26, 2026 13:57
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.Ref parsing, validation/feature-flag gating, path expansion/resolution, and project-file emission via injected <ProjectReference>.
  • Update dotnet run virtual project construction to pre-create/register referenced virtual projects, and update caching decisions to avoid #:ref scenarios.
  • Extend dotnet project convert and tests/docs/resources to support converting #:ref graphs 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.

Comment thread src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs Outdated
Comment thread src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs
Comment thread src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs
Comment thread documentation/general/dotnet-run-file.md Outdated
Comment thread documentation/general/dotnet-run-file.md Outdated
Comment thread src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs Outdated
@jjonescz
Copy link
Copy Markdown
Member Author

jjonescz commented Apr 2, 2026

@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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we require they start with either #! or have #:property OutputType=Library somewhere among the directives of the entry point file?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have #:property OutputType=Library somewhere 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

@RikkiGibson RikkiGibson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done review pass

Comment thread src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs Outdated
Comment thread src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs Outdated
Comment thread src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs Outdated
Comment thread src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs
Comment thread src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs Outdated
Comment thread test/dotnet.Tests/CommandTests/Run/RunFileTests.cs
Comment thread test/dotnet.Tests/CommandTests/Run/RunFileTests.cs
Comment thread test/dotnet.Tests/CommandTests/Run/RunFileTests.cs
Comment thread test/dotnet.Tests/CommandTests/Run/RunFileTests.cs
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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have #:property OutputType=Library somewhere 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.

@jjonescz
Copy link
Copy Markdown
Member Author

jjonescz commented Apr 8, 2026

@333fred for another review, thanks

@jjonescz
Copy link
Copy Markdown
Member Author

@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 };
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens on Linux when I do:

#:ref Lib.cs
#:ref lib.cs

Copy link
Copy Markdown
Member Author

@jjonescz jjonescz Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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('\\', '/')));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens in the presence of symlinks?

Copy link
Copy Markdown
Member Author

@jjonescz jjonescz Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@jjonescz jjonescz requested a review from 333fred April 14, 2026 09:11
@jjonescz
Copy link
Copy Markdown
Member Author

/ba-g known: #53796, #53789

@jjonescz jjonescz merged commit b278293 into dotnet:release/10.0.3xx Apr 15, 2026
24 of 28 checks passed
@jjonescz jjonescz deleted the 52532-ref-directive-2 branch April 15, 2026 08:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-run-file Items related to the "dotnet run <file>" effort

Projects

None yet

Development

Successfully merging this pull request may close these issues.

File-based apps should be able to reference other file-based libraries via #:ref

7 participants