Proposal for platform-specific apps for .NET 8
See original GitHub issueProposal for platform-specific apps for .NET 8
We’ve been on a journey to rationalize dotnet build
and dotnet publish
in terms of producing platform-specific apps and how the CLI exposes a natural progression of app specialization options. The overall product started out without sufficient design on this topic and we’ve been trying to remedy that ever since. We believe we can finally correct these design challenges with .NET 8.
Notes:
build
is used a short-hand forbuild
andpublish
unless specifically called out otherwise.- “platform-specific” is used as a synonym for “RID-specific”.
- Intended as a concrete plan for much of https://github.com/dotnet/sdk/issues/26446.
Context
Framework-dependent apps have been the default build
output type since .NET Core 1.0. That remains the best default in our view. The primary deployment model for .NET is a globally installed runtime, and framework-dependent apps align with that. We should retain this behavior.
In many scenarios, an app will only ever run in one platform environment (like Linux + x64), and developers know that a priori (for example for containers or client apps). There can be performance benefits by taking advantage of that. In addition, apps built for a single environment are simpler (fewer assets and flat directory structure). We should make it easier to produce platform-specific apps, ideally making it the default.
In contrast, in other scenarios, developers benefit from apps being portable across operating system and architectures. That model has been the default to date and is arguably an advanced pseudo-magic behavior. This behavior should be opt-in since it comes with important trade-offs that developers should understand (or that publishing workflows provide as a requirement).
Platform specialization
Developers can specialize an app for a given platform by specifying a Runtime ID (RID) when building an app, with the -r
argument, like with:
dotnet build -r linux-x64
Specializing an app with a RID produces a self-contained app. This design choice has always been unfortunate, since RID specialization equally applies to framework-dependent and self-contained apps.
The SDK provides a warning when a RID is specified without any additional arguments related to output type. This warning was added with .NET 6.
root@efbeccd6a45b:/app# dotnet build -r linux-arm64
MSBuild version 17.4.1+9a89d02ff for .NET
Determining projects to restore...
Restored /app/app.csproj (in 5.16 sec).
/usr/share/dotnet/sdk/7.0.102/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.Sdk.targets(1142,5): warning NETSDK1179: One of '--self-contained' or '--no-self-contained' options are required when '--runtime' is used. [/app/app.csproj]
The intent of this warning was enabling an eventual change in the default behavior when specifying a RID. Given that the warning has been in place for two versions, including an LTS version, we can reasonably now make the change.
Starting with apps that target .NET 8:
- Building with a RID will produce a framework-dependent app.
- If
--self-contained
or--self-contained true
is specified, then a self-contained app will be produced. Same when theSelfContained=true
. - Warning
NETSDK1179
will no longer be produced. - For earlier versions, We should consider changing the wording of the warning to make clear that the behavior has changed for .NET 8+ apps.
As the current warning suggests, developers should specify --self-contained
in response for this change, if they haven’t already.
Platform specializing by default
We can further refine this proposal by specializing apps by default. We also need to consider compatibility, and an opt-out to enable producing portable apps.
In fact, there is already a CLI argument that provides this behavior:
root@efbeccd6a45b:/app# dotnet build --help | grep runtime
--use-current-runtime Use current runtime as the target
runtime.
We could add a property with that name, but we can probably do better.
Instead, we propose a TargetRuntime
property that pairs with TargetFramework
. It will be a convenience wrapper over RuntimeIdentifier
, meaning that it will always produce a valid value (including empty string) for that property.
TargetRuntime
will allow the following values:
portable
– same as specifying no RID and procurrent
– same as specifying--use-current-runtime
- any valid RID
.NET 8 templates will be updated to include this property, set to current
, making .NET 8 apps RID-specific by default.
The follow project file demonstrates the change for the console
template.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetRuntime>current</TargetRuntime>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
We are aware that there are two variants of “current RID”, one short, one long.
The --use-current-runtime
flag results in an OS-specific RID, like osx-arm64
, while dotnet --info
prints a RID with a version, as follows.
% dotnet --info | grep RID
RID: osx.13-arm64
That seems odd and asymmetric. Ideally, both variants would be accessible from within a project file and the CLI. More generally, we don’t have good names for these different styles of RIDs. We don’t need to resolve that for this feature, but it is something that we should separately look into.
Portable apps and apphost
Portable apps include the apphost by default. That doesn’t really make sense since the apphost is platform-specific, while the rest of the app is intended to work anywhere. However, it’s possible that developers appreciate having an apphost for local dev.
There is no harm to shipping an apphost with a portable app. It is very small and very unlikely to be the source of a vulnerability. However, the platform it supports is arbitrary with respect to the platforms it might run on. As a result, it is hard to describe a general use case (beyond development).
We could adopt the following model:
- Portable apps do not include an apphost, the same behavior as early .NET Core versions.
- Developers that want an apphost (the existing behavior) can set
UseAppHost=true
. - Developer that want an apphost only for development can set
UseDebugAppHost=true
(which would be a new property). An alternative would beUseAppHost=Debug
.
As an aside, we notice many Dockerfile
examples where apps are launched with the dotnet foo.dll
pattern, even those they don’t need to. Lauching an app with the dotnet
launcher is a fine pattern and should certainly be embraced by portable apps, since it’s the only correct cross-platform pattern.
Issue Analytics
- State:
- Created 8 months ago
- Reactions:2
- Comments:10 (9 by maintainers)
Top GitHub Comments
The portable apphost change isn’t about space saving. It’s about defining the scenario for the apphost (for portable apps). I claim that it is very narrow and that having it confuses things. If it wasn’t there, it would help people better understand what portable apps are for.
Perhaps it isn’t obvious, but the apphost is NOT portable.
Damian’s recollection is correct! Though the idea of doing it in .NET 9 is very up in the air.