.NET RuntimeIdentifier vs RuntimeIdentifiers

Written on December 5, 2019

A Runtime Identifier (RID) is used to identify target platforms where a .NET Core application runs. They come into play when packages contain platform-specific assets (eg. native code for Linux, or Windows 64bit).

You can specify a single RID using the <RuntimeIdentifier> element in the project file, or to specify multiple RIDs use <RuntimeIdentifiers>.

Many dotnet command also can specify --runtime (or -r).

According to the documentation, if you only need to specify a single runtime then using <RuntimeIdentifier> will also result in faster builds.

I’ve noticed some other subtle difference between the singular and plural forms of this element.

Let’s create a simple .NET Core console app:

md rid
cd rid
dotnet new console

By default, the csproj (named rid.csproj in this case) looks like this:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

</Project>

If you look inside the obj directory, you’ll find a file named rid.csproj.nuget.dgspec.json. Its contents look like this:

{
  "format": 1,
  "restore": {
    "C:\\tmp\\rid\\rid.csproj": {}
  },
  "projects": {
    "C:\\tmp\\rid\\rid.csproj": {
      "version": "1.0.0",
      "restore": {
        "projectUniqueName": "C:\\tmp\\rid\\rid.csproj",
        "projectName": "rid",
        "projectPath": "C:\\tmp\\rid\\rid.csproj",
        "packagesPath": "C:\\Users\\david\\.nuget\\packages\\",
        "outputPath": "C:\\tmp\\rid\\obj\\",
        "projectStyle": "PackageReference",
        "fallbackFolders": [
          "C:\\Program Files\\dotnet\\sdk\\NuGetFallbackFolder"
        ],
        "configFilePaths": [
          "C:\\Users\\david\\AppData\\Roaming\\NuGet\\NuGet.Config",
          "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config"
        ],
        "originalTargetFrameworks": [
          "netcoreapp3.1"
        ],
        "sources": {
          "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {},
          "https://api.nuget.org/v3/index.json": {}
        },
        "frameworks": {
          "netcoreapp3.1": {
            "projectReferences": {}
          }
        },
        "warningProperties": {
          "warnAsError": [
            "NU1605"
          ]
        }
      },
      "frameworks": {
        "netcoreapp3.1": {
          "imports": [
            "net461",
            "net462",
            "net47",
            "net471",
            "net472",
            "net48"
          ],
          "assetTargetFallback": true,
          "warn": true,
          "frameworkReferences": {
            "Microsoft.NETCore.App": {
              "privateAssets": "all"
            }
          },
          "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\3.1.100\\RuntimeIdentifierGraph.json"
        }
      }
    }
  }
}

If you supply a runtime identifier when running restore, like dotnet restore -r win10-x64, then two extra sections are added to this file. Firstly, under the "netcoreapp3.1" node:

          "downloadDependencies": [
            {
              "name": "Microsoft.AspNetCore.App.Runtime.win-x64",
              "version": "[3.1.0, 3.1.0]"
            },
            {
              "name": "Microsoft.NETCore.App.Runtime.win-x64",
              "version": "[3.1.0, 3.1.0]"
            },
            {
              "name": "Microsoft.WindowsDesktop.App.Runtime.win-x64",
              "version": "[3.1.0, 3.1.0]"
            }
          ]

and secondly under the second "C:\\tmp\\rid\\rid.csproj" node, a runtimes section is added:

      "runtimes": {
        "win10-x64": {
          "#import": []
        }
      }

If you instead passed in -r linux-x64 then predictably, those entries refer to linux-x64 instead of win-x64.

Adding <RuntimeIdentifier>win10-x64</RuntimeIdentifier> to the csproj and running dotnet restore has exactly the same effect as if you specified the RID on the command line.

And now running dotnet build with the RID specified results in the compiled application being created in bin\Debug\netcoreapp3.1\win10-x64. Plus, since .NET Core 3 it also defaults to creating a self-contained application (so you get an .exe as well as all the dependent assemblies to allow you to run the application on a machine that didn’t already have the runtime installed)

It’s a slightly different story if you use <RuntimeIdentifiers> though..

You can’t specify multiple RIDs on the command line (well actually in .NET Core 2.2 you could for restore, but not in 3). So let’s change our csproj to have <RuntimeIdentifiers>win10-x64;linux-x64</RuntimeIdentifiers>. and run

dotnet restore

the dgspec.json now contains entries for both platforms. eg.

          "downloadDependencies": [
            {
              "name": "Microsoft.AspNetCore.App.Runtime.linux-x64",
              "version": "[3.1.0, 3.1.0]"
            },
            {
              "name": "Microsoft.AspNetCore.App.Runtime.win-x64",
              "version": "[3.1.0, 3.1.0]"
            },
            {
              "name": "Microsoft.NETCore.App.Host.linux-x64",
              "version": "[3.1.0, 3.1.0]"
            },
            {
              "name": "Microsoft.NETCore.App.Runtime.linux-x64",
              "version": "[3.1.0, 3.1.0]"
            },
            {
              "name": "Microsoft.NETCore.App.Runtime.win-x64",
              "version": "[3.1.0, 3.1.0]"
            },
            {
              "name": "Microsoft.WindowsDesktop.App.Runtime.win-x64",
              "version": "[3.1.0, 3.1.0]"
            }
          ],

and

      "runtimes": {
        "linux-x64": {
          "#import": []
        },
        "win10-x64": {
          "#import": []
        }
      }

but now if you run dotnet build, something interesting… there’s no bin\Debug\netcoreapp3.1\win10-x64 or bin\Debug\netcoreapp3.1\linux-x64 directories like you might be expecting. Instead there’s just the regular compiled assembly in bin\Debug\netcoreapp3.1! Almost as if you’d never set a RID at all.

What you can do now though, is build for both platforms consecutively. eg.

dotnet build -r win10-x64
dotnet build -r linux-x64

and you get both self-contained builds for win10-x64 and linux-x64 platforms! Plus, as you’ve already done a restore, you can make the build faster by passing in --no-restore so it doesn’t bother trying to restore again.

So if you’re targetting a single platform, use -r on the command-line or <RuntimeIdentifier>. If you’re targetting multiple platforms, use <RuntimeIdentifiers> and then use separate restore and build steps

Categories: .NET