dotnet / dotnet/sdk

Make file-based apps multi-language

Open
#55,716 5 comments 0 reactions 0 assignees View on GitHub
Area-run-file
Dominant language
C#
Stars
3.2k
Forks
1.3k
PR merge metrics
PR metrics pending

Description

`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](https://github.com/dotnet/fsharp/pull/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.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with the SDK/CLI file-based app pipeline behind `dotnet run file.cs` and its existing `#:ref` virtual-project graph; the payload does not name implementation files or tests. Trace how entry-point and referenced-file languages, cross-language `#:project`/`#:ref`, cycle errors, editor tooling, and debugging would be handled, with completion defined by the requested cross-language graph behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp, fsharp
Domain
build-system, cli, developer-experience
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.