dotnet run file.cs runs a single C# file with no project. This asks to make the file-based app pipeline language-agnostic:
dotnet run app.fs runs a single F# file, the same way dotnet run app.cs runs a C# one.
#:ref and #:project can cross languages, in both directions.
The language side is already done: C# ignores #: (C# 14), and F# just merged the same in dotnet/fsharp#20212 (RFC-1337). VB and other languages can opt in later, and the design must not block them.
Motivation
A big ask from the F# community is C# source generators. Today F# users build an intermediate C# project only to run a generator, for example [GeneratedRegex]. A web of single-file components removes that extra project and the need to maintain it.
Other technologies and techniques fit one language better than another, for example low-level programming in C# and domain modelling with types in F#. Needing nothing more than a new file would greatly improve the multi-language experience of .NET.
This proposal of multi-language file graphs supports a smooth experience: pick the best language for each task, while staying on a COMMON language runtime and SDK.
Core requirement: a language-aware #:ref graph
Today #:ref builds one virtual project per referenced file, follows transitive references, and already rejects cycles. Keep that model and make it language-aware:
- each file's virtual project uses the language of its extension,
#:ref and #:project resolve across languages, in both directions,
- a reference cycle stays an error.
One project per file is already single-language, so no compaction is needed for correctness.
Example: a web of .cs and .fs
TopLevelEntryPoint.cs (C#) -> #:ref BusinessLogic.fs
BusinessLogic.fs (F#) -> #:ref DomainTypes.fs, SourceGeneratedRegexes.cs
SourceGeneratedRegexes.cs (C#) // uses [GeneratedRegex], C#-only
DomainTypes.fs (F#)
BusinessLogic.fs needs a source-generated regex, which only C# offers, so it references SourceGeneratedRegexes.cs. The entry point is C# and uses BusinessLogic.fs. Each file becomes its own single-language virtual project.
Requested scope
- SDK/CLI: pick the language from the file extension for both the entry point and each referenced file. Build the language-aware
#:ref graph.
- Editor (Roslyn LSP + F# tooling): a file-based app resolves to a set of cross-language virtual projects, so IntelliSense, go-to-definition, and diagnostics work across a cross-language
#:ref.
- Debugger: attach and debug a file-based app whose graph spans languages.
Out of scope
A VB language change (later).
Open questions
- How a reference cycle is reported to the CLI and the editor.
Bonus: potentially compacting the graph
One project per file works but multiplies projects and builds. As an optional optimization, the SDK could emit fewer projects, as long as each stays single-language and the reference graph stays acyclic. This is not "one project per language".
Take the example above. The fewest projects is 3, because the two C# files cannot share one:
| Project |
Lang |
Files |
Depends on |
| A |
C# |
SourceGeneratedRegexes.cs |
— |
| B |
F# |
DomainTypes.fs, BusinessLogic.fs |
A |
| C |
C# |
TopLevelEntryPoint.cs |
B |
SourceGeneratedRegexes.cs and TopLevelEntryPoint.cs are both C#, but BusinessLogic.fs (F#) sits between them (C → B → A). Merging the two C# files would form a cycle with the F# project. So C# splits into two projects, while the two F# files merge into one. The algorithm, its minimality target, and a deterministic tie-break stay open.
Remark: replace hard-coded C# rules with pluggable components
The SDK should stop hard-coding C#. Abstract the C#-specific rules behind separated interfaces, with one language-specific handler per language, for (a) entry-point/run selection, (b) virtual-project construction, and (c) directive parsing/translation. The core stays language-neutral and drives the graph, the compaction, and the build, so new languages need no changes to shared code.
dotnet run file.csruns a single C# file with no project. This asks to make the file-based app pipeline language-agnostic:dotnet run app.fsruns a single F# file, the same waydotnet run app.csruns a C# one.#:refand#:projectcan cross languages, in both directions.The language side is already done: C# ignores
#:(C# 14), and F# just merged the same in dotnet/fsharp#20212 (RFC-1337). VB and other languages can opt in later, and the design must not block them.Motivation
A big ask from the F# community is C# source generators. Today F# users build an intermediate C# project only to run a generator, for example
[GeneratedRegex]. A web of single-file components removes that extra project and the need to maintain it.Other technologies and techniques fit one language better than another, for example low-level programming in C# and domain modelling with types in F#. Needing nothing more than a new file would greatly improve the multi-language experience of .NET.
This proposal of multi-language file graphs supports a smooth experience: pick the best language for each task, while staying on a COMMON language runtime and SDK.
Core requirement: a language-aware
#:refgraphToday
#:refbuilds one virtual project per referenced file, follows transitive references, and already rejects cycles. Keep that model and make it language-aware:#:refand#:projectresolve across languages, in both directions,One project per file is already single-language, so no compaction is needed for correctness.
Example: a web of
.csand.fsBusinessLogic.fsneeds a source-generated regex, which only C# offers, so it referencesSourceGeneratedRegexes.cs. The entry point is C# and usesBusinessLogic.fs. Each file becomes its own single-language virtual project.Requested scope
#:refgraph.#:ref.Out of scope
A VB language change (later).
Open questions
Bonus: potentially compacting the graph
One project per file works but multiplies projects and builds. As an optional optimization, the SDK could emit fewer projects, as long as each stays single-language and the reference graph stays acyclic. This is not "one project per language".
Take the example above. The fewest projects is 3, because the two C# files cannot share one:
SourceGeneratedRegexes.csDomainTypes.fs,BusinessLogic.fsTopLevelEntryPoint.csSourceGeneratedRegexes.csandTopLevelEntryPoint.csare both C#, butBusinessLogic.fs(F#) sits between them (C → B → A). Merging the two C# files would form a cycle with the F# project. So C# splits into two projects, while the two F# files merge into one. The algorithm, its minimality target, and a deterministic tie-break stay open.Remark: replace hard-coded C# rules with pluggable components
The SDK should stop hard-coding C#. Abstract the C#-specific rules behind separated interfaces, with one language-specific handler per language, for (a) entry-point/run selection, (b) virtual-project construction, and (c) directive parsing/translation. The core stays language-neutral and drives the graph, the compaction, and the build, so new languages need no changes to shared code.