📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What are runtime identifiers (RIDs), and when do you need to set one in a .NET project?

Runtime identifiers: portable RIDs like linux-x64 and win-arm64, when a RID is needed (self-contained, single-file, AOT, native assets), RuntimeIdentifier versus RuntimeIdentifiers, the .NET 8 move to portable RIDs, and RID-specific NuGet assets.

Intermediate Theory Intermediate deployment native-dependencies project-system rid runtime-identifier

🎯 Key points

  • A RID names a target platform as os-architecture, such as win-x64, linux-x64, linux-musl-x64, linux-arm64, osx-arm64
  • Portable framework-dependent apps need no RID; the runtime picks native assets at run time from runtimes/<rid>/ folders
  • Set a RID for self-contained, single-file, ReadyToRun and Native AOT output, or to produce a platform-specific app
  • RuntimeIdentifier sets one RID for build and publish; RuntimeIdentifiers lists RIDs to restore for without fixing one
  • Since .NET 8 the SDK uses a portable-only RID graph: distro-specific RIDs like ubuntu.22.04-x64 fail with NETSDK1083 unless you opt back into the old graph with UseRidGraph=true, which is a temporary escape hatch
  • linux-musl-x64 is for Alpine; using linux-x64 there fails at run time
  • Since .NET 8, -r no longer implies self-contained

⏱ 30-second answer

A runtime identifier, or RID, names the platform you build for, as OS plus architecture: win-x64, linux-x64, linux-musl-x64 for Alpine, linux-arm64 or osx-arm64. A normal framework-dependent app doesn't need one. It stays portable, and packages with native code ship runtimes/<rid>/ folders that the runtime picks from at start-up. You set a RID when the output must be platform-specific: self-contained, single-file, ReadyToRun or Native AOT. Since .NET 8 the SDK only understands portable RIDs, so distro-specific ones like ubuntu.22.04-x64 no longer work. Also since .NET 8, -r alone doesn't make the app self-contained.

🗣 Full answer (senior)

How I would answer this in an interview

"A runtime identifier names the platform you build for as OS plus architecture: win-x64, linux-x64, linux-arm64, osx-arm64, and linux-musl-x64 for Alpine. It's different from an OS-specific TFM: net10.0-windows chooses which APIs you compile against, while win-x64 chooses where the output runs.

A normal framework-dependent app doesn't need one. It stays portable, and packages with native code ship runtimes/<rid>/ folders that the host picks from at start-up. I set a RID when the output must be platform-specific: self-contained, single-file, ReadyToRun or Native AOT. RuntimeIdentifier fixes one RID for every build and publish, while RuntimeIdentifiers only tells restore to prepare assets for several, so I can restore once and publish each with -r and --no-restore.

Two .NET 8 changes still apply. The SDK uses a portable-only RID graph, so distro-specific RIDs like ubuntu.22.04-x64 fail with NETSDK1083; UseRidGraph brings the old graph back, but only as a temporary escape hatch. And -r no longer implies self-contained, so I pass --self-contained explicitly. The mistake I see is publishing linux-x64 for an Alpine image: musl isn't glibc, so a native library fails to load at run time. So I keep RIDs out of the default build, match the RID to the base image, and add a start-up smoke test in CI."


The format

<os>[-<libc>]-<arch>:

RID Platform
win-x64, win-arm64 Windows
linux-x64, linux-arm64 glibc-based Linux (Debian, Ubuntu, RHEL, Azure Linux)
linux-musl-x64, linux-musl-arm64 musl-based Linux, mainly Alpine
osx-x64, osx-arm64 macOS Intel and Apple silicon

When you need one

Scenario RID needed?
Framework-dependent, portable (dotnet publish) No
Framework-dependent but platform-specific (smaller output, native apphost for one OS) Optional: -r linux-x64
Self-contained Yes
Single-file, ReadyToRun, Native AOT Yes
SDK container publish Uses one (it defaults to the container's platform)

How native assets are chosen

Packages such as Microsoft.Data.SqlClient or SkiaSharp ship native libraries per platform:

runtimes/
  win-x64/native/libfoo.dll
  linux-x64/native/libfoo.so
  linux-musl-x64/native/libfoo.so
  osx-arm64/native/libfoo.dylib
  • Portable app: every folder is copied to the output, and the host picks the right one at start-up using deps.json.
  • RID-specific app: only the matching folder is copied, so output is smaller.

RuntimeIdentifier versus RuntimeIdentifiers

  • <RuntimeIdentifier>linux-x64</RuntimeIdentifier> fixes the RID for every build and publish of the project. Output goes to bin/<Config>/<TFM>/linux-x64/.
  • <RuntimeIdentifiers>linux-x64;win-x64</RuntimeIdentifiers> tells restore to prepare assets for those RIDs, so dotnet publish -r win-x64 --no-restore works later, without changing the default build.

The .NET 8 changes

  1. Portable RIDs only: the SDK and runtime stopped using the big RID graph with distro versions. ubuntu.22.04-x64 or rhel.8-x64 now produce errors or warnings; use linux-x64.
  2. -r doesn't imply self-contained: pass --self-contained explicitly.

Both still apply in .NET 10.

Production example

A worker published with -r linux-x64 ran fine on Ubuntu VMs but crashed on start-up in a new Alpine-based image, with a native library load error from a compression package. Alpine uses musl, so the image needed linux-musl-x64. Publishing with the right RID (or moving to a glibc base image) fixed it, and a smoke test in the pipeline now starts the container before it's pushed.

🔍 What the interviewer is checking

Checks platform awareness for deployment.

Green flags: knows the RID format and musl versus glibc; explains when a RID is needed; knows the .NET 8 portable-RID and self-contained changes; understands runtimes/ native assets.

Red flags: sets a RID on every project by habit; uses distro-specific RIDs; can't explain why an app fails on Alpine.

📚 Learn in depth

1. Anatomy of a RID

   linux - musl - arm64
     |      |       |
     |      |       +-- architecture: x64, x86, arm64, arm
     |      +---------- C library (Linux only): omitted = glibc, musl = Alpine and similar
     +----------------- operating system: win, linux, osx, (ios, android, browser ...)
RID Runs on
win-x64, win-x86, win-arm64 Windows
linux-x64, linux-arm64, linux-arm glibc-based Linux: Debian, Ubuntu, RHEL, Azure Linux, Amazon Linux
linux-musl-x64, linux-musl-arm64 musl-based Linux, mainly Alpine
osx-x64, osx-arm64 macOS on Intel and Apple silicon

These are portable RIDs. Since .NET 8 they are the only ones the SDK and runtime understand by default.

2. What changes when you set a RID

 PackageReference (with runtimes/* assets)
          |
       restore  -- RuntimeIdentifiers=linux-x64;win-x64
          |         --> RID-specific assets recorded in obj/project.assets.json
          |         --> runtime packs downloaded if self-contained
          |
       build    -- RuntimeIdentifier=linux-x64
          |         --> bin/Release/net10.0/linux-x64/
          |         --> apphost for Linux, linux-x64 native files only
          |
       publish  -- --self-contained --> + the .NET runtime for linux-x64
                -- PublishSingleFile --> one executable for linux-x64
                -- PublishAot        --> native executable for linux-x64

 portable (no RID):
     bin/Release/net10.0/
        App.dll, App.deps.json, App.runtimeconfig.json
        App / App.exe          <- apphost for the build machine's OS
        runtimes/win-x64/native/...
        runtimes/linux-x64/native/...
        runtimes/osx-arm64/native/...
     the host reads App.deps.json at start-up and loads the matching folder
Aspect No RID (portable) With a RID
Output folder bin/<Config>/<TFM>/ bin/<Config>/<TFM>/<RID>/ (unless AppendRuntimeIdentifierToOutputPath=false)
Native assets All platforms copied Only the matching platform
Apphost For the OS you build on For the target RID
Self-contained possible No Yes, with --self-contained
Runs on Any OS with the .NET runtime That OS and architecture only

3. When you need a RID

Scenario RID? Why
Framework-dependent web API in a container or App Service No (optional) The runtime is already there, and portable output works everywhere
Smaller framework-dependent output for one OS Optional Drops other platforms' native files
Self-contained Yes The SDK must know which runtime to bundle
Single-file Yes The bundle includes a platform-specific host
ReadyToRun Yes Precompiled code is per architecture
Native AOT Yes The output is a native binary
SDK container publish (-t:PublishContainer) Chosen for you Defaults to the base image's platform; ContainerRuntimeIdentifier overrides it
A RID-specific .NET tool package (new in .NET 10) Yes ToolPackageRuntimeIdentifiers lists the platforms to pack

Instead of -r, you can pass --os linux --arch arm64 and the SDK builds the RID for you. --arch alone uses the current OS.

4. RuntimeIdentifier versus RuntimeIdentifiers

RuntimeIdentifier (singular) RuntimeIdentifiers (plural)
Affects Restore, build and publish Restore only
Default build Becomes RID-specific Stays portable
Typical use A project that only ever ships for one platform Restoring once in CI, then publishing for several platforms with -r ... --no-restore
Command-line equivalent -r linux-x64 (none; set in the project or Directory.Build.props)

Publishing with --no-restore for a RID that wasn't restored fails with NETSDK1047, which asks you to add the RID to RuntimeIdentifiers.

5. Native assets in packages

Packages with native code (SQLite, SkiaSharp, gRPC, some compression and crypto libraries) ship one folder per platform:

runtimes/
  win-x64/native/e_sqlite3.dll
  linux-x64/native/libe_sqlite3.so
  linux-musl-x64/native/libe_sqlite3.so
  linux-arm64/native/libe_sqlite3.so
  osx-arm64/native/libe_sqlite3.dylib
  • A portable app copies all of them, and the host picks one at start-up.
  • A RID-specific app copies only the match, falling back along the portable graph (for example linux-musl-x64 falls back to linux-x64, then linux, then unix, then any).
  • If no folder matches, the app starts but fails when it first calls the native library, with DllNotFoundException or a "cannot open shared object file" error.

6. The .NET 8 RID changes (still true in .NET 10)

  1. Portable RID graph only. The old graph had distro- and version-specific RIDs (ubuntu.22.04-x64, rhel.8-x64, alpine.3.18-x64). The SDK now reports NETSDK1083 for them. <UseRidGraph>true</UseRidGraph> restores the old graph as a temporary workaround, mainly for old packages that only ship distro-specific assets. Move to packages with portable assets and remove the workaround.
  2. -r no longer implies self-contained. Before .NET 8, dotnet publish -r linux-x64 produced a self-contained app; now it's framework-dependent unless you add --self-contained.
  3. RuntimeInformation.RuntimeIdentifier returns the portable RID (linux-x64), not the distro RID, which is the right value to log.

7. Cross-building

Output Build on a different OS? Build for a different architecture?
Framework-dependent Yes Yes
Self-contained, single-file Yes Yes
ReadyToRun Yes, with some limits Yes
Native AOT No: build on the target OS Possible with extra setup (for example a Linux arm64 cross toolchain and sysroot, or Visual Studio's arm64 build tools on Windows)

For AOT and multi-platform releases, CI usually runs a matrix of agents (Windows, Linux x64, Linux arm64, macOS).

8. RIDs and containers

Base image RID
mcr.microsoft.com/dotnet/aspnet:10.0 (Debian or Ubuntu based, depending on tag) linux-x64 or linux-arm64
mcr.microsoft.com/dotnet/aspnet:10.0-alpine linux-musl-x64 or linux-musl-arm64
Chiseled images (10.0-noble-chiseled) linux-x64 or linux-arm64
runtime-deps images (for self-contained or AOT) Match the image's libc and architecture

When you build the image with a Dockerfile on an Apple silicon Mac or an arm64 agent, the default platform is arm64. Use --platform linux/amd64 (or build for both) if production runs on x64.

9. Diagnosing RID problems

Symptom Likely cause Fix
NETSDK1083 "The specified RuntimeIdentifier is not recognized" A distro-specific RID Use a portable RID such as linux-x64
NETSDK1047 "Assets file doesn't have a target for ..." Publishing with --no-restore for a RID that wasn't restored Add it to RuntimeIdentifiers, or restore with -r
DllNotFoundException or "cannot open shared object file" The native asset for this platform is missing, often musl versus glibc Publish with the right RID, or change the base image
"exec format error" in a container Wrong architecture (arm64 binary on x64 or the reverse) Match -r and the image platform
App is larger than expected Portable output with every platform's native files Publish with the target RID
Local build output moved to a RID folder RuntimeIdentifier set in the project Move it to publish time, or use RuntimeIdentifiers

Log RuntimeInformation.RuntimeIdentifier, RuntimeInformation.OSArchitecture and RuntimeInformation.OSDescription at start-up so every native load failure comes with the platform in the log.

10. Performance, cost and security

  • Self-contained and AOT outputs pin a runtime version. You must republish for every .NET security patch; framework-dependent apps pick up patches when the host or base image is updated.
  • Arm64 VMs and containers are often cheaper per unit of compute in Azure and AWS. A linux-arm64 build plus a performance test can be an easy saving.
  • RID-specific output removes other platforms' native binaries, which shrinks images and reduces what a vulnerability scanner reports.

11. .NET 11 awareness

The portable RID model is stable. Watch the .NET 11 release notes for changes to supported OS versions and architectures, and for any new RIDs, before relying on them. Keep production builds on .NET 10 until you've tested the upgrade.

12. Try it yourself

  1. Create a console app, run dotnet publish -o out/portable, and list out/portable (no RID folder).
  2. Add Microsoft.Data.Sqlite, publish again and look inside out/portable/runtimes/.
  3. Run dotnet publish -r linux-x64 -o out/linux and compare: only the Linux native file remains.
  4. Run dotnet publish -r ubuntu.22.04-x64 and read NETSDK1083.
  5. Run dotnet publish -r linux-musl-x64 --self-contained -o out/alpine and compare its size with step 3.
  6. Add <RuntimeIdentifiers>linux-x64;win-x64</RuntimeIdentifiers>, restore once, then publish both with --no-restore.
  7. Print RuntimeInformation.RuntimeIdentifier in the app and run it on your machine and in an Alpine container.

💻 Code

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <!--
      RuntimeIdentifiers (plural) only tells restore to prepare assets for these platforms.
      Normal builds stay portable; we pick a platform at publish time with -r.
      Using RuntimeIdentifier (singular) here would make every local build linux-specific.
    -->
    <RuntimeIdentifiers>linux-x64;linux-musl-x64</RuntimeIdentifiers>
  </PropertyGroup>
</Project>
# Restore once for both platforms
dotnet restore

# Debian/Ubuntu-based image (glibc)
dotnet publish -c Release -r linux-x64 --self-contained --no-restore -o out/linux-x64

# Alpine-based image (musl): a different RID because native libraries differ
dotnet publish -c Release -r linux-musl-x64 --self-contained --no-restore -o out/linux-musl-x64
// Log the platform at start-up: invaluable when a native load fails
using System.Runtime.InteropServices;

Console.WriteLine($"RID {RuntimeInformation.RuntimeIdentifier}, OS {RuntimeInformation.OSDescription}");

⚠ Common mistakes

  • Mistake: publishing with linux-x64 for an Alpine image. Alpine uses musl, not glibc, so native libraries built for linux-x64 fail to load, often only when a code path first calls them. Instead: use linux-musl-x64 (or linux-musl-arm64) for Alpine, or switch to a Debian, Ubuntu or chiseled base image and keep linux-x64.

  • Mistake: using distro-specific RIDs such as ubuntu.22.04-x64. Since .NET 8 the SDK only understands portable RIDs, so these fail with NETSDK1083. Copying them from old documentation or old pipelines breaks the build. Instead: use portable RIDs (linux-x64). Use UseRidGraph=true only as a short-term workaround for an old package, and plan to remove it.

  • Mistake: setting RuntimeIdentifier in the project when you only need it for one publish. Every local build becomes platform-specific, the output folder moves to bin/<Config>/<TFM>/<RID>/, and scripts that expect the old path break. Instead: keep builds portable and pass -r at publish time. If you need restore to be ready for several platforms, use RuntimeIdentifiers.

  • Mistake: assuming -r makes the app self-contained. Since .NET 8, dotnet publish -r linux-x64 produces a framework-dependent app. It then fails to start on a machine or image without the .NET runtime. Instead: pass --self-contained explicitly when you want the runtime bundled, or use a base image that already has it.

  • Mistake: publishing with --no-restore for a RID that wasn't restored. The assets file has no entry for that RID, so publish fails with NETSDK1047. Instead: list every publish RID in RuntimeIdentifiers before the restore step, or restore with the same -r value.

  • Mistake: building Native AOT for Linux on a Windows agent. Native AOT doesn't cross-compile between operating systems, so the build fails or needs a Linux toolchain that isn't there. Instead: build AOT on the target OS, using a CI matrix with Linux and Windows agents, or build inside a Linux container.

  • Mistake: ignoring the CPU architecture of the build machine and the host. An image built on an Apple silicon Mac or arm64 agent is arm64 by default, and fails on x64 hosts with "exec format error". The reverse happens when moving to cheaper arm64 instances. Instead: set the RID and the image platform explicitly (-r linux-x64, --platform linux/amd64), or build multi-architecture images.

  • Mistake: confusing OS-specific TFMs with RIDs. net10.0-windows chooses which APIs you compile against; win-x64 chooses which platform the output runs on. Setting one doesn't set the other. Instead: use an OS-specific TFM when you need platform APIs such as WPF, and a RID when you need platform-specific output.

  • Mistake: forgetting that self-contained and AOT apps don't get runtime patches automatically. The runtime is baked into the output, so updating the host or base image doesn't fix a runtime vulnerability. Instead: rebuild and redeploy self-contained and AOT apps on every .NET security release, and track them in your patching process.

  • Mistake: not logging the platform at start-up. When a native library fails to load, there's no record of which RID, OS and architecture the app was actually running on. Instead: log RuntimeInformation.RuntimeIdentifier, OSArchitecture and OSDescription at start-up, and add a container smoke test in CI that starts the app before the image is pushed.

🏛 Senior insight: trade-offs

Most services should stay portable and framework-dependent, so the RID question only arises for specific deployment shapes. When it does, a senior engineer keeps RIDs out of the project's default build (using RuntimeIdentifiers for restore and -r at publish), matches the RID exactly to the base image (glibc versus musl, x64 versus arm64), and adds a start-up smoke test so native-asset mistakes are caught in CI rather than in production. With Arm64 instances often cheaper per unit of compute in Azure, being deliberate about linux-arm64 builds can also save real money.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate says self-contained, single-file or AOT output needs a RID. Why: Moves from RIDs to the publish shapes that need them.
    How to answer

    By default dotnet publish gives a framework-dependent folder that needs the .NET runtime on the server. Self-contained (--self-contained -r linux-x64) bundles the runtime, so nothing needs installing, but the output is bigger and you patch the runtime by redeploying. On top of that: single-file packs everything into one executable, trimming removes unused code, ReadyToRun precompiles to speed up start-up, and Native AOT compiles to a native binary with very fast start-up and low memory but no dynamic code generation. The SDK can also build a container image directly. I choose based on where it runs, start-up and memory needs, and whether the libraries are trim- and AOT-safe, and I measure the result.

  2. When: Candidate mixes up net10.0-windows with win-x64. Why: Checks the difference between OS-specific TFMs and RIDs.
    How to answer

    A target framework moniker, or TFM, such as net10.0, tells the SDK which API surface to compile against and which runtime the app needs. OS-specific TFMs like net10.0-windows add platform APIs such as WPF. For applications I target one framework, normally the newest LTS: that's net10.0 today, supported until November 2028. .NET 8 and .NET 9 both reach end of support on 10 November 2026, so they need upgrading now. Libraries can multi-target with <TargetFrameworks>net8.0;net10.0</TargetFrameworks> while they still have consumers on older versions. That builds the library once per framework, and #if NET10_0_OR_GREATER handles API differences. .NET 11 is a preview, so it's for experiments only.

↩ Usually asked after

More in Project System

Last updated 1 Nov 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me