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

📚 Interview question

What are target framework monikers like net8.0, net9.0 and net10.0, how do you choose one, and when should you multi-target?

TFMs and the .NET support lifecycle: what net8.0/net9.0/net10.0 and OS-specific TFMs mean, LTS versus STS, choosing targets for apps versus libraries, multi-targeting with TargetFrameworks and conditional code, and .NET 11 awareness.

Intermediate Theory Intermediate dotnet-lifecycle multi-targeting project-system target-framework tfm

🎯 Key points

  • A TFM (net10.0) names the API surface a project compiles against and the runtime it needs
  • OS-specific TFMs such as net10.0-windows add platform APIs (WinForms, WPF); net10.0-android and net10.0-ios are for mobile
  • .NET 10 is LTS (three years of support); STS releases now get 24 months; .NET 8 and .NET 9 both reach end of support on 10 November 2026
  • Applications target one TFM, normally the newest LTS; libraries may multi-target with <TargetFrameworks> to serve older consumers
  • Multi-targeting builds the project once per TFM; use #if NET10_0_OR_GREATER or conditional ItemGroups for differences
  • netstandard2.0 is for code that must run on several implementations: .NET Framework, Unity and other Mono-based runtimes, and Roslyn analysers and source generators, which must target it
  • .NET 11 previews are awareness only until release; don't target previews in production

⏱ 30-second 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.

🗣 Full answer (senior)

How I would answer this in an interview

"A target framework moniker, like net10.0, names the API surface a project compiles against and the runtime it needs. It's separate from the SDK version: global.json picks the SDK, the TFM picks what the output runs on. OS-specific TFMs such as net10.0-windows add platform APIs like WPF and WinForms, and net10.0-android or net10.0-ios are for mobile; that's a different thing from a runtime identifier, which names the OS and CPU you publish for.

Choosing is lifecycle management. .NET 10 is LTS with three years of support, STS releases now get 24 months, and .NET 8 and .NET 9 both reach end of support on 10 November 2026, so anything still on them needs upgrading now. For applications I target one framework, normally the newest LTS. Libraries can multi-target with <TargetFrameworks>net8.0;net10.0</TargetFrameworks> while consumers are still moving; that builds once per target, the package gets a lib folder for each, and #if NET10_0_OR_GREATER handles API differences.

netstandard2.0 still has real uses: code that must run on .NET Framework or Unity, and Roslyn analysers and source generators, which must target it. The mistake I see is multi-targeting applications, which only doubles build and test time for a runtime you'll never deploy. .NET 11 previews belong on a branch, not in production. I keep the TFM in Directory.Build.props, so an upgrade is one line plus fixes."


What a TFM is

<TargetFramework>net10.0</TargetFramework> tells the SDK to compile against the .NET 10 reference assemblies and to write a runtimeconfig.json that asks for the .NET 10 runtime. Common forms:

TFM Meaning
net10.0 .NET 10, cross-platform APIs
net10.0-windows Adds Windows-only APIs (needed for WPF/WinForms with UseWPF/UseWindowsForms)
net10.0-android, net10.0-ios .NET MAUI and mobile workloads
netstandard2.0 A contract implemented by .NET Framework 4.6.1+, all modern .NET, Mono, Xamarin and Unity
net48 .NET Framework 4.8

Support lifecycle (as at October 2026)

Version Type End of support
.NET 8 LTS 10 November 2026
.NET 9 STS (extended to 24 months) 10 November 2026
.NET 10 LTS November 2028
.NET 11 STS, in preview Awareness only until it ships

LTS releases get three years of patches. In September 2025 Microsoft extended STS support from 18 to 24 months, starting with .NET 9, which is why .NET 8 and .NET 9 end on the same day. After that date there are no security patches, which matters for anything handling personal information under the Privacy Act or aligned with the Essential Eight's patching controls.

Choosing

  • Applications and services: one TFM, the newest LTS (net10.0). Upgrade on a schedule, not when support has already ended.
  • Libraries consumed by others: target the lowest version your consumers need and the newest, for example net8.0;net10.0, then drop net8.0 once consumers have moved.
  • netstandard2.0 still has real uses: libraries that must also run on .NET Framework, libraries for Unity or other Mono-based runtimes, and Roslyn analysers and source generators, which must target netstandard2.0 because they load inside compilers and IDEs running on different runtimes. Accept its smaller API surface only when you need that reach.

Multi-targeting

<TargetFrameworks>net8.0;net10.0</TargetFrameworks>

Note the plural element. The SDK builds the project once per TFM and the NuGet package gets one lib/<tfm>/ folder each. For differences:

  • Preprocessor symbols: NET10_0, NET10_0_OR_GREATER, NET8_0_OR_GREATER, NETSTANDARD2_0.
  • Conditional items: <ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">.
  • Polyfill packages for small API gaps.

Multi-targeting multiplies build and test time (tests should run against every target), so only do it when someone needs it.

Production example

A shared Contoso.Telemetry library targeted netstandard2.0 long after every consumer had moved to modern .NET, so it couldn't use newer APIs and its consumers carried compatibility packages. Retargeting to net8.0;net10.0 removed three dependencies and let it use System.Threading.Lock and newer System.Text.Json features on .NET 10, with #if NET10_0_OR_GREATER keeping the .NET 8 build working for two teams that were still migrating.

🔍 What the interviewer is checking

Checks understanding of TFMs and lifecycle, not just syntax.

Green flags: explains what the TFM controls; knows LTS/STS and current end-of-support dates; distinguishes app and library targeting; uses TargetFrameworks and #if NETx_0_OR_GREATER correctly.

Red flags: multi-targets everything; doesn't know .NET 8 support is ending; thinks netstandard is the modern default.

📚 Learn in depth

1. What a TFM controls

<TargetFramework>net10.0</TargetFramework>
        |
        +--> reference assemblies   Microsoft.NETCore.App.Ref 10.0 (the API surface you compile against)
        +--> preprocessor symbols   NET, NET10_0, NET10_0_OR_GREATER, NET9_0_OR_GREATER, NET8_0_OR_GREATER ...
        +--> NuGet asset selection  lib/net10.0, else the nearest compatible folder
        +--> runtimeconfig.json     "framework": { "name": "Microsoft.NETCore.App", "version": "10.0.0" }
        +--> default C# version     C# 14 for net10.0, C# 13 for net9.0, C# 12 for net8.0
        +--> analyser defaults      AnalysisLevel follows the TFM, so newer targets get newer rules

The TFM is about the API surface and runtime, not the SDK. The .NET 10 SDK builds net8.0, net9.0 and net10.0 projects; global.json chooses the SDK, the TFM chooses what the output runs on.

2. Anatomy of a TFM

TFM Parts Meaning
net10.0 framework + version .NET 10, cross-platform APIs only
net10.0-windows + OS Adds Windows APIs; needed for WPF and WinForms (UseWPF, UseWindowsForms)
net10.0-windows10.0.19041.0 + OS version Adds Windows Runtime APIs available from that Windows build
net10.0-android, net10.0-ios, net10.0-maccatalyst + OS .NET MAUI and mobile workloads
net10.0-browser + OS Browser WebAssembly
netstandard2.0 contract An API contract implemented by .NET Framework 4.6.1+, every modern .NET, Mono, Xamarin and Unity
netstandard2.1 contract Larger contract, not implemented by .NET Framework
net48, net472 .NET Framework Note: no dot, and different from net4.8 style names you sometimes see written by hand

For OS TFMs, SupportedOSPlatformVersion sets the minimum OS version you support (it can be lower than the version in the TFM, which only sets which APIs you compile against).

3. The support lifecycle (as at October 2026)

Version Type Released End of support
.NET 8 LTS November 2023 10 November 2026
.NET 9 STS (24 months) November 2024 10 November 2026
.NET 10 LTS November 2025 November 2028
.NET 11 STS Due November 2026 (release candidates now) About November 2028
.NET 12 LTS (expected) Expected November 2027

Rules of thumb:

  • A new major version ships every November. Even numbers are LTS (three years), odd numbers are STS (24 months since .NET 9).
  • Patches arrive on "Patch Tuesday" each month. Only the latest patch of a supported version is supported.
  • After end of support there are no security fixes. For systems that handle personal information, or that follow the Essential Eight's patching controls, running an unsupported runtime is a finding in any audit.

4. Choosing a target

 Is it an application or service you deploy?
    yes --> one TFM: the newest LTS (net10.0). Plan the next LTS upgrade early.
    no  --> it's a library. Who consumes it?
              only your own apps on .NET 10      --> net10.0
              apps still moving from .NET 8      --> net8.0;net10.0 (drop net8.0 soon)
              .NET Framework consumers too       --> netstandard2.0 (+ net10.0 for newer APIs)
              Roslyn analyser / source generator --> netstandard2.0 (required)
              Unity or other Mono runtimes       --> netstandard2.0 or netstandard2.1

5. How NuGet picks the right folder

A package can contain several lib/<tfm>/ folders. NuGet picks the nearest compatible one for the consuming project:

Consumer Package has NuGet uses
net10.0 net8.0, net10.0 net10.0
net9.0 net8.0, net10.0 net8.0 (nearest lower version)
net10.0 netstandard2.0 only netstandard2.0
net48 netstandard2.0, net8.0 netstandard2.0
net48 net8.0 only Error NU1202: not compatible
net10.0-windows net10.0, net10.0-windows net10.0-windows

This is why a library that multi-targets net8.0;net10.0 still serves a net9.0 consumer: it gets the net8.0 build.

6. Multi-targeting: what actually happens

<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
        |
   outer build ---+--> inner build (TargetFramework=net8.0)  --> bin/Release/net8.0/
   (dispatches)   +--> inner build (TargetFramework=net10.0) --> bin/Release/net10.0/
        |
   dotnet pack  --> one .nupkg with lib/net8.0/ and lib/net10.0/
   dotnet test  --> runs the tests once per target framework
  • Every inner build restores, compiles and runs analysers separately, so build time grows with each target.
  • dotnet build -f net10.0 builds one target only, which is handy locally.
  • Properties and items can differ per target with conditions on $(TargetFramework).
  • To compare TFMs in conditions, prefer $([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0')) over string comparison when you mean "this target or newer".

Handling API differences

Technique Use when Example
#if symbols Small code differences #if NET10_0_OR_GREATER ... #else ... #endif
Conditional items A dependency only one target needs <ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
Separate files Larger differences Clock.net8.cs and Clock.net10.cs, each included conditionally
Polyfill packages Missing attributes or small APIs on older targets Packages that backfill required, init or IsExternalInit for older targets
Runtime checks OS differences inside one TFM if (OperatingSystem.IsWindows()), with [SupportedOSPlatform("windows")] on the API

The common symbols: NET, NETCOREAPP, NETFRAMEWORK, NETSTANDARD, versioned forms such as NET10_0 and NET8_0_OR_GREATER, and OS symbols such as WINDOWS, ANDROID and IOS for OS-specific TFMs.

7. Runtime roll forward

A framework-dependent net8.0 app asks for the .NET 8 runtime. By default it runs on the newest installed 8.0.x patch; it does not move to .NET 10 on its own.

RollForward value Effect
Minor (default) Newest patch of the requested major; a higher minor if the requested one is missing
LatestPatch Only patches of the exact major.minor
Major A higher major version if the requested one isn't installed
LatestMajor Always the highest installed version
Disable Exactly the requested version

Major lets an old app start on a newer runtime, but it hasn't been tested there. When a version reaches end of support, the fix is to retarget and test, not to rely on roll forward.

8. Upgrading a target framework

  1. Put the TFM in Directory.Build.props (for example <TargetFramework>net10.0</TargetFramework>) so the change is one line for the whole repo.
  2. Pin the new SDK in global.json.
  3. Read the breaking-changes page for every version you skip (going from .NET 8 to .NET 10 means reading the .NET 9 and .NET 10 lists).
  4. Update Microsoft packages (Microsoft.Extensions.*, Microsoft.AspNetCore.*, EF Core) to the matching major version.
  5. Build, fix new warnings (newer analysers come with the newer TFM), and run all tests.
  6. Update Docker base images, CI agent images and hosting (App Service stack, Functions runtime) to the new runtime.
  7. Deploy to a non-production environment and watch performance and error rates before production.

9. Library-specific checks

  • Turn on <EnablePackageValidation>true</EnablePackageValidation> so dotnet pack checks that every target exposes a compatible API, and set PackageValidationBaselineVersion to catch breaking changes against the last release.
  • Run tests for every target framework in CI, not just the newest.
  • Removing a target framework is a breaking change for consumers on it. Announce it and bump the major version.

10. .NET 11 awareness

.NET 11 (net11.0) is due in November 2026 as an STS release with 24 months of support. Try it in a branch with its own global.json, or add net11.0 to a library's TargetFrameworks in a preview build only. Keep production applications on net10.0 (LTS) until .NET 11 has shipped and you have tested it, and remember that many organisations prefer to move from LTS to LTS (.NET 10 to .NET 12).

11. Try it yourself

  1. Create a class library and set <TargetFrameworks>net8.0;net10.0</TargetFrameworks> (plural).
  2. Add a method that uses #if NET10_0_OR_GREATER, build, and look in bin/Debug/ for two folders.
  3. Run dotnet pack and open the .nupkg (it's a zip) to see lib/net8.0 and lib/net10.0.
  4. Run dotnet build -f net8.0 to build only one target.
  5. Create a net9.0 console app that references the library and check which folder it uses in obj/project.assets.json.
  6. Change the singular <TargetFramework> to net8.0;net10.0 and read the error.
  7. In a console app, open bin/Debug/net10.0/*.runtimeconfig.json and read the framework version and roll-forward setting.

💻 Code

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>net8.0;net10.0</TargetFrameworks>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <!-- Only the net8.0 build needs this polyfill package -->
  <ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
    <PackageReference Include="Contoso.Polyfills" Version="1.2.0" />
  </ItemGroup>
</Project>
namespace Contoso.Telemetry;

public sealed class Counter
{
#if NET10_0_OR_GREATER
    private readonly Lock _gate = new();
#else
    private readonly object _gate = new();
#endif
    private long _value;

    public long Increment()
    {
        lock (_gate)
        {
            return ++_value;
        }
    }
}

System.Threading.Lock arrived in .NET 9, so NET9_0_OR_GREATER would also work; the example uses the .NET 10 symbol because those are the two targets.

⚠ Common mistakes

  • Mistake: writing several targets in the singular <TargetFramework> element. <TargetFramework>net8.0;net10.0</TargetFramework> is treated as one invalid TFM and the build fails. Instead: use the plural <TargetFrameworks>net8.0;net10.0</TargetFrameworks> for multiple targets, and the singular form for one.

  • Mistake: staying on a TFM after its end of support. .NET 8 and .NET 9 both stop receiving security patches on 10 November 2026. After that, every new vulnerability stays open, which is an audit finding for systems that handle personal information. Instead: move applications to net10.0 (LTS) now, starting with internet-facing services, and plan each upgrade well before the end-of-support date.

  • Mistake: multi-targeting applications. An app runs on one runtime in production, so extra targets only double build and test time and add #if noise. Instead: give applications one TFM, and multi-target only libraries that have consumers on different versions.

  • Mistake: keeping netstandard2.0 out of habit. It hides newer APIs and performance improvements, and consumers often need extra compatibility packages. Instead: use netstandard2.0 only when you need it (.NET Framework or Unity consumers, Roslyn analysers and source generators), and otherwise target modern TFMs such as net8.0;net10.0 or just net10.0.

  • Mistake: targeting a preview (.NET 11) in production code. Previews and release candidates can change APIs and behaviour, and have limited support. Instead: try net11.0 in a branch with its own global.json, and keep production on net10.0 until .NET 11 has shipped and you have tested the upgrade.

  • Mistake: raising LangVersion above the default for an older TFM. Some C# features need runtime or library support, so code compiles but fails or behaves differently at run time on the older target. It is also unsupported. Instead: leave LangVersion unset so it follows each target framework, and use polyfill packages only for features that are known to work.

  • Mistake: expecting a net8.0 app to move to the .NET 10 runtime by itself. Framework-dependent apps roll forward only to the newest patch of their own major version by default. On a server with only .NET 10 installed, a net8.0 app won't start at all. Instead: retarget to net10.0 and test. Use RollForward set to Major only as a short, deliberate stopgap.

  • Mistake: confusing the SDK version with the target framework. People assume a net8.0 project needs the .NET 8 SDK, or that installing the .NET 10 SDK upgrades the app. Instead: pin the SDK in global.json and the target in <TargetFramework>. The .NET 10 SDK builds net8.0 projects, but the output still needs the .NET 8 runtime.

  • Mistake: testing only the newest target of a multi-targeted library. Code inside #if branches for older targets is never run, so bugs reach the consumers who are still on them. Instead: let dotnet test run every target framework in CI (it does by default when the test project multi-targets too).

  • Mistake: comparing TFMs with string conditions. Condition="'$(TargetFramework)' == 'net10.0'" stops matching when you add net10.0-windows or move to net11.0. Instead: use $([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0')) when you mean "this version or newer", and exact string matches only when you mean exactly one target.

  • Mistake: dropping a target framework from a library in a minor release. Consumers on that framework suddenly can't update, and NuGet reports NU1202 after what looked like a safe upgrade. Instead: treat removing a TFM as a breaking change: announce it, and release it in a new major version.

🏛 Senior insight: trade-offs

TFM choice is lifecycle management. A senior engineer keeps a simple policy: applications on the current LTS, an upgrade planned well before end of support, libraries targeting exactly what their consumers need and no more. With .NET 8 and .NET 9 both ending on 10 November 2026, that policy is urgent right now: an estate still on those versions needs a prioritised upgrade plan, starting with internet-facing services and anything processing personal information. They centralise the TFM in Directory.Build.props so an upgrade is one line plus fixes, and they track .NET 11 previews on a branch only, to learn what's coming without exposing production.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate mentions OS-specific TFMs or platform-specific output. Why: Separates the API target (TFM) from the platform target (RID).
    How to 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.

  2. When: Candidate says a net8.0 project needs the .NET 8 SDK. Why: Checks the candidate doesn't confuse the SDK version with the target framework.
    How to answer

    The dotnet host looks for a global.json from the current folder upwards. If there isn't one, it uses the latest installed SDK, even a preview. If there is one, it reads sdk.version and applies the rollForward policy to choose from the installed SDKs. With a version and no policy, the default is latestPatch, meaning the same feature band with the newest patch. allowPrerelease controls whether previews count. If nothing matches, every SDK command fails. I pin the SDK in every repo so developers, IDEs and CI build with the same compiler, analysers and defaults. dotnet --info shows which SDK was picked and why.

  3. When: Candidate multi-targets a library. Why: Follows multi-targeting through to the NuGet package consumers receive.
    How to answer

    dotnet pack builds the project and produces a .nupkg whose metadata comes from MSBuild properties in the project: PackageId, Version, Authors, Description, a licence expression, a repository URL and a README. I set the version from CI using Semantic Versioning, so a breaking change means a new major version. I publish symbols as a .snupkg with Source Link, so consumers can step into the exact source. I turn on package validation to catch accidental breaking changes. Then dotnet nuget push uploads to nuget.org or an internal feed, and organisations that need it can sign packages with dotnet nuget sign.

↩ Usually asked after

More in Project System

Last updated 1 Nov 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me