dotnet / dotnet/sdk

file-based apps: define deterministic conflict handling for non-duplicate #: directives in multi-file composition

Open
#54,090 1 comment 0 reactions 0 assignees View on GitHub
Area-run-file untriaged
Dominant language
C#
Stars
3.2k
Forks
1.3k
PR merge metrics
PR metrics pending

Description

### Is your feature request related to a problem? Please describe.

Using `#:include` for multi-file file-based apps exposes a second class of problems beyond duplicate include inputs:

- same `#:package` with different versions
- same `#:property` with different values
- conflicting `#:sdk` values

These are not simple duplicates. They are semantic conflicts.

Today, some of these cases still build/run, often with warnings or behavior that appears to depend on evaluation/import order. That makes reusable multi-file composition fragile and hard to reason about.

---

### Current observed behavior

In local experiments with multi-file file-based apps:

1. `#:package` same package, different versions
Example: `Newtonsoft.Json@13.0.3` vs `@12.0.3`
Typical result: `NU1504`, but build/run may still succeed.

2. `#:property` same property, different values
Example: `PublishAot=false` vs `PublishAot=true`, or conflicting `DefineConstants`
Typical result: often no hard error, and the effective result may appear overwrite-like.

3. `#:sdk` conflicting values
Example: `Microsoft.NET.Sdk.Web` vs `Microsoft.NET.Sdk`
Typical result: `MSB4011`, but some builds/runs may still succeed.

This leaves users in an awkward state: the system is permissive enough that some conflicting cases still work, but not deterministic enough to rely on that behavior.

---

### Why this matters

`#:include` makes file-based apps meaningfully multi-file, but multi-file composition needs predictable directive semantics.

Without deterministic conflict handling, users are pushed toward:

- manual pre-processing,
- scenario-specific entry generation,
- or ejecting to a full `.csproj` earlier than they would like.

That weakens the lightweight composition story of file-based apps.

---

### Describe the solution you'd like

A good next step would be to define a deterministic conflict handling model for **non-duplicate directives** in multi-file composition.

In particular:

1. **Conflict classification**
- clearly define which non-duplicate conflicts are:
- safe to merge
- warning-level but deterministic
- hard errors / fail-fast

2. **Entry-file-directed behavior**
- the entry file should remain the primary place for concrete global directive intent
- included files can still be reusable, but unsafe conflicts should not silently resolve by incidental processing order

3. **Explicit merge rules where supported**
- if some directive kinds or property kinds are intended to support merging, document those cases explicitly
- otherwise prefer fail-fast over accidental precedence

4. **Build-time conditional directive participation**
- conditional participation of directives would help avoid forcing mutually incompatible directives into the same effective closure

5. **Clear escalation path**
- lightweight same-closure reuse stays with `#:include`
- when a real boundary is needed, a stronger boundary model such as `#:ref` / separate project reference becomes the appropriate direction

---

### What I am not asking for

I am **not** asking for:

- runtime conditional inclusion inside normal C# control flow
- a separate full project system on top of file-based apps
- a complex per-module compatibility declaration language

In fact, requiring every included file to fully declare package/property compatibility would create too much authoring burden.

A better direction seems to be:

- keep the model entry-file-first
- define deterministic conflict handling
- add conditional directive participation where appropriate
- use stronger boundaries when unification is not appropriate

---

### Temporary mitigations today

In current preview SDK usage, the practical mitigations seem to be:

- keep global / concrete directives concentrated in the entry file
- keep included files as lightweight and non-global as possible
- treat current “build succeeds anyway” conflict behavior as non-contractual
- use `NoWarn=CS2002` only as a temporary mitigation for duplicate include warnings
- use separate project / process boundaries when a single include closure is not a good fit

Also, although the current spec already describes `#:ref` as a stronger-boundary direction, in my local `11.0.100-preview.3` SDK it is not yet recognized by the parser, so it is not currently available as a practical workaround in this environment.

---

### Related issues / spec context

- Duplicate include deduplication: [#54080](https://github.com/dotnet/sdk/issues/54080)
- Directive extensions / conditional directives discussion: [#52532](https://github.com/dotnet/sdk/issues/52532)
- Current spec: [`dotnet-run-file.md` ](https://github.com/dotnet/sdk/blob/main/documentation/general/dotnet-run-file.md)

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.