dotnet / dotnet/sdk

Ideas for extending file-based apps directives

Open
#52,532 3 comments 6 reactions 2 assignees Claimed by @jjonescz View on GitHub
Area-run-file untriaged
Dominant language
C#
Stars
3.2k
Forks
1.3k
PR merge metrics
PR metrics pending

Description

This issue is a container for a set of loosely related ideas for extending file-based apps directives beyond what's currently supported to enable more scenarios without needing to eject to a traditional project.

## Supporting name=value metadata on item-producing directives

Related: #52399

There are cases where the simple high-level directives currently available that project to MSBuild items in the virtual project aren't usable because extra metadata has to be configured, e.g. referencing a package but adjusting which assets are included in the project (as described in #52399).

For such directives we could allow defining extra metadata via the familiar `name=value` syntax and properly support optional escaping of values with double or single quotes to allow for values that contain whitespace in an unambiguous way, e.g.:

```csharp
#:project '../shared/Some Class Lib Dir With Space In its name' BuildReference=false
#:package Microsoft.Build@1.2.3 ExcludeAssets="runtime;build" PrivateAssets=true
```

The metadata attribute name could only use characters valid in an XML attribute name (alpha, hyphen, underscore, etc.) and values containing whitespace would require escaping with quotes.

## Supporting generic directive for defining items

Related #48174 #48782

There is currently no directive for defining build items in file-based apps beyond those projected by the supported directives, i.e. `#:project -> ` , `#:package -> `, `#:sdk -> ` (technically not an item like the others but similar semantics). Supporting scenarios that require the ability to define items thus requires defining them in an implicit build file (`Directory.Build.targets`), creating and publishing a custom MSBuild SDK that can be referenced, or ejecting to a traditional project.

Support for multi-file file-based apps (#48174) proposes higher level concepts such as implicit item type includes based on glob file extension and a new directive like `#:include`, but having a lower-level directive that higher-level concepts "lower" to would enable many more scenarios to be accessible from file-based apps.

A proposed syntax for defining arbitrary items could be:

```csharp
#:item:Compile Include=$(AppFileName).*.cs
```

The directive is itself a prefix of `#:item:` followed immediately by the item to be defined, e.g. `Compile`, after which extra name-value pairs can be defined using the syntax proposed earlier for name=value metadata pairs. The item name could only use characters valid in an XML element name (alpha, underscore, hyphen, etc.). The separator between the `#:item` prefix and the item name could be something other than `:` if required, e.g. some alternatives:

```csharp
// Whitespace separator
#:item Compile Include=$(AppFileName).*.cs
```

```csharp
// Parenthesis
#:item(Compile) Include=$(AppFileName).*.cs
```

```csharp
// Arrow
#:item>Compile Include=$(AppFileName).*.cs
```

```csharp
// At symbol
#:item@Compile Include=$(AppFileName).*.cs
```

Higher level directives could still be supported that effectively lower to this generic item directive. For example, the proposed `#:include` directive (#48782) could lower itself like this:

```csharp
// Implicit include directive:
#:include $(AppFileName).*.cs
#:include 'file needed at runtime.json'
#:include $(AppFileName).*.resx
// Lowers to:
#:item:Compile Include=$(AppFileName).*.cs
#:item:Content 'file needed at runtime.json'
#:item:Resource Include=$(AppFileName).*.resx
```

Another scenario is referencing loose assemblies (#49129) which could be enabled via a new `#:assembly ../path/to/assembly.dll` directive that lowers to the `#:item` directive:

```csharp
// Assembly reference
#:assembly ../lib/SomeLibrary.dll
// Lowers to
#:item:Reference Include=../lib/SomeLibrary.dll
```

We could also simply support referencing assemblies via the implicit item type support of the proposed `#:include` directive, e.g:

```csharp
// Assembly reference
#:include ../lib/SomeLibrary.dll
// Lowers to
#:item:Reference Include=../lib/SomeLibrary.dll
```

## Supporting conditions via block directives

In some scenarios it is beneficial or even necessary to be able to use directives conditionally, e.g. when multi-targeting. While conditions could be supported using the proposed support for metadata name=value pairs described earlier (and given the projection, it would be), it might be desirable to support a block-style syntax to easily apply the same condition to multiple directives or simply for aesthetic reasons, e.g.:

```csharp
#:property TargetFrameworks=net10.0;net9.0
#:if $(TargetFramework)==net9.0
#:package System.Text.Json@10.*
#:property EnableSomething=true
#else
#:property EnableSomethingElse=true
#:project ../ModernHelpers
#:endif
```

The `#:if` block would support a condition expression whose value would be pre-processed to ensure correct quoting of property expressions and values on either side of the operator, before being projected to become the value of a `Condition` attribute, thus allowing for a clearer definition (feeling less like manual string matching). Inside the block only other `#:` directives and C# comments would be allowed. Directives would be projected into either an `` or `` as is appropriate for the directive. For example, the block above would project to the following in the virtual project:

```xml

net10.0;net9.0

true

true

```

## Supporting references between file-based apps

Unlike the proposal for multiple files in file-based apps, some scenarios require referencing a file-based app from another file-based app, e.g. unit testing, performance benchmarks, shared libraries, etc. To enable these scenarios, we should consider adding a `#:ref` directive that allows for this. The directive would be projected to a `` item in the virtual project and would also convert the referenced app when `dotnet project convert` is run on the referencing app.

For example an `app.tests.cs` testing app that contains tests for an `app.cs` file-based app:

```csharp
#:package xunit.v3@3.2.2
#:ref app.cs

// Unit test code...
...
```

The reasoning behind the naming of `#:ref` is that the other logical "reference" directives are all based on the noun of the thing being referenced. In the case of a file-based app referencing another file-based app though, it feel natural to simply use an otherwise unqualified reference, i.e. apps can `#:ref` each other, but you reference other things via their natural higher-level directives, e.g. `#:package`, `#:sdk`, `#:project`, etc. It also doesn't create oddness when referencing a file-based app that actually produces a library (rather than an executable).

Alternatively we could consider introducing separate `#:app` and `#:lib` directives, that in reality are interchangeable and do the exact same thing as `#:ref` is proposed to do.

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.