# Dotnet Arm64 Migration

> Migrate one or more .NET projects to run on ARM64 (Apple Silicon, Windows on ARM, Linux/Alpine arm64, cloud arm64 like AWS Graviton / Azure Ampere). Use when the user wants to add arm64 support, target ARM64, build/publish for an arm64 runtime identifier (win-arm64, linux-arm64, linux-musl-arm64, osx-arm64), fix x86/x64 assumptions, resolve missing arm64 native NuGet assets, guard x86 hardware intrinsics, or run on Graviton / Ampere / Apple Silicon. User-initiated — match the user's stated intent to move a codebase onto ARM64. Not for general framework or package version upgrades.

- Skill: `microsoft/dotnet-arm64-migration` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add microsoft/dotnet-arm64-migration`
- Raw SKILL.md: https://api.skillmd.com/api/skills/microsoft/dotnet-arm64-migration/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: Microsoft (https://skillmd.com/u/microsoft)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/microsoft/dotnet-arm64-migration

---


# .NET ARM64 Migration Scenario

Make one or more .NET projects build, publish, and run correctly on an ARM64 runtime
identifier (`win-arm64`, `linux-arm64`, `linux-musl-arm64`, `osx-arm64`), fixing the
project-file, NuGet native-asset, source-code, and infrastructure surfaces that assume
x86/x64.

## Scenario Overview

**Goal**: Move the scoped project(s) onto ARM64 — correct RID/platform settings, resolve
arm64-capable native package assets, guard x86-only hardware intrinsics, and surface the
remaining awareness/concurrency findings — validated by a cross-compile Gate (and native
run/test when the host itself is arm64).

This scenario is **user-initiated**. The user asks to target ARM64; act on the scope they
name. Do not auto-migrate unrelated projects.

**What this scenario proves and does not prove.** Milestone A delivers detection, the
mechanical fixes, and a cross-compile **Gate** that proves *restore + native-asset
resolution + IL compilation* for the target RID. The Gate does **not** prove that native
libraries load, that P/Invoke entry points resolve, or that concurrency behaves correctly
on arm64 — those require execution on matching-RID hardware (native run, emulated Smoke, or
CI). Always tell the user this boundary; never present a green Gate as arm64 correctness
certification.

**Language coverage.** The project-file/RID (`0001`–`0003`), NuGet native-asset
(`0004`/`0005`), and infrastructure (`0010`) surfaces are language-agnostic and apply to
both C# and Visual Basic projects. The **source-code** rules (`0006`–`0009` and the code
portion of `0012`) analyze **C# only** — Visual Basic projects do not receive
architecture-sensitive source-code analysis (x86 intrinsics, native-interop, bitness, or
concurrency hazards). For a VB project, manually review those hazards; the automated scan
will not surface them.

## Workflow Stages

Run these stages in order:

0. **Pre-Initialization** — Confirm scope + target RID(s); detect host OS+arch; run the
   **.NET Framework → modern-.NET decision gate** (and the **< 4.8.1 viability hard-block**)
   before assessment. Uses the `scenario-initialization` system skill.
1. **Assessment** — Detect the four ARM64 surfaces (project / NuGet / code / infra) into
   `assessment.md` (+ JSON). Tool: `generate_arm64_migration_assessment`.
2. **Planning** — Triage findings into auto-fixable / guided / flag-only; order the
   `0001`↔`0012` fixes; decide package bump-vs-replace. Produces the planning artifacts.
3. **Execution** — Apply the project-file, RID, package, and intrinsic-guard fixes; leave
   review/flag findings as annotated tasks. Uses the executor's core task-execution steps.
4. **Validation** — Host-adaptive ladder: cross-compile Gate always; native build/run/test
   when the host matches the target RID.

## Pre-Initialization

This section is used by the `scenario-initialization` system skill. It defines the
scenario-specific parameters for this scenario.

### Parameters to Confirm

⛔ **Step 1 — Scope.** Determine the scope from the user's request and normalize it to a
concrete set of projects:
- A single **project** → that project.
- **Several projects** → those projects.
- A **folder** → every project under it.
- A **solution** (`.sln`/`.slnx`) → its projects.
- The **whole repo** → all projects in the repo.

Use `get_solution_path` / `get_projects_info` to discover and confirm the project set. Pass
the scope to the assessment tool as `inputMode` (`solution` | `projects` | `folder`) and
`paths` (semicolon-delimited).

⛔ **Step 2 — Target RID(s).** Confirm the arm64 runtime identifier(s) to target, a subset
of `{ win-arm64, linux-arm64, linux-musl-arm64, osx-arm64 }`:
- If the user named a target (e.g. "Graviton" → `linux-arm64`, "Apple Silicon" → `osx-arm64`,
  "Windows on ARM" → `win-arm64`, "Alpine/musl" → `linux-musl-arm64`), use it.
- Otherwise **do not guess** — leave `targetRids` unset and let the tool infer per project
  from project type and current RIDs, then confirm the inferred set with the user.
- RIDs are **always lowercase**. A RID that does not match a project's OS family is rejected
  per project by the tool — never force a cross-OS RID onto a project.
- `linux-musl-arm64` is **never inferred**; only target it when the user explicitly asks for
  musl/Alpine, because a glibc `linux-arm64` native asset is not reliably loadable on musl.

⛔ **Step 3 — .NET Framework decision gate (before assessment).** Cheaply read each project's
TFM. For any project targeting **.NET Framework**, surface the modernize-first recommendation
as an explicit decision gate — do **not** proceed silently:
- **(a) Accept modernization** → hand off to the `dotnet-version-upgrade` scenario **first**. Because
  there is no automatic scenario-chaining primitive, how you re-enter ARM64 depends on the **Flow Mode**:
  - **Guided Flow Mode** — stop after the version upgrade completes and tell the user to **re-invoke
    this ARM64 scenario**. Do **not** silently auto-resume the ARM64 intent; leave the ARM64 migration
    for a fresh, user-initiated run so the modernization can be reviewed on its own.
  - **Automatic Flow Mode** — you may run `dotnet-version-upgrade` and then continue into this ARM64
    scenario in the same pass, but only if you: (1) **record it explicitly as a two-phase effort**
    (Phase 1 = `dotnet-version-upgrade`, Phase 2 = `dotnet-arm64-migration`) in
    `scenario-instructions.md` and the run summary; and (2) **re-run the ARM64 assessment against the
    modernized project** before applying any arm64 fix — never carry findings over from a
    pre-modernization assessment (the TFM, RIDs, and project style all change during modernization).
- **(b) Decline modernization** → proceed with ARM64 on .NET Framework, **but** apply the
  **< 4.8.1 viability hard-block first**: arm64 is only supported on .NET Framework 4.8.1+.
  A project below 4.8.1 that declines modernization is **stopped at the gate** with a clear
  message ("ARM64 needs .NET Framework 4.8.1+ or modernization to .NET") rather than being
  given unbuildable arm64 fixes. Remove that project from scope.
- **Mixed solution** (some Framework, some modern): apply the gate **per Framework project**.
  Modern projects proceed straight to assessment unaffected.

⛔ **Step 4 — Exclusions.** Collect any projects removed from scope at this gate (e.g. a
`< 4.8.1` project that declined modernization). Pass them to the assessment tool as
`excludedProjects` (semicolon-delimited), or downgrade a `solution`/`folder` scope to
`inputMode=projects` with only the surviving paths — so the tool never assesses a blocked
project.

**Step 5** — Proceed with the confirmed scope, target RID(s), and exclusions, passing them
to `initialize_scenario` and then to the assessment stage.

## Stage Instructions

⛔ **IMPORTANT**: Load each stage's instructions file **only when entering that stage** (not
all upfront).

### Stage 1: Assessment
**When entering this stage, load**: [assessment.md](assessment.md)

Runs `generate_arm64_migration_assessment` over the confirmed scope, detecting the four
ARM64 surfaces (project-file settings, NuGet native assets, source-code hazards, infra) and
writing `assessment.md` (+ JSON).

### Stage 2: Planning
**When entering this stage, load**: [planning.md](planning.md)

Triages each finding into auto-fixable / guided / flag-only, correlates `Arm64.0001` with
`Arm64.0012` to decide the ordered platform-target fix, and chooses bump-vs-replace for
native packages. Produces `plan.md` and `scenario-instructions.md` in the
canonical format.

### Stage 3: Execution
**When entering this stage, load**: [execution.md](execution.md)

Applies the mechanical fixes (project-file, RID, safe package bumps, intrinsic guards) and
leaves review/flag findings as annotated tasks, validating with builds. Uses the executor's core
task-execution steps.

### Stage 4: Validation
**When entering this stage, load**: [validation.md](validation.md)

Runs the host-adaptive validation ladder: the cross-compile Gate always, plus native
build/run/test when the host OS+arch (and libc flavor for musl) matches the target RID.

## Success Criteria

- [ ] Every scoped project declares an arm64 RID for its target OS with correct lowercase casing.
- [ ] `PlatformTarget=x86` / `Prefer32Bit=true` removed where safe, or handled as a guided
      task when 32-bit interop (`Arm64.0012`) is present.
- [ ] NuGet packages with native assets are at an arm64-capable version (safe bump) or the
      guided replacement/major-bump decision is recorded.
- [ ] Unguarded `System.Runtime.Intrinsics.X86` uses have an `…IsSupported` guard + fallback.
- [ ] Awareness (`0007`/`0008`) and concurrency (`0009`) findings are surfaced as review tasks.
- [ ] The cross-compile **Gate** (build + publish for the target RID) passes for every scoped
      project — and the user is reminded this proves packaging/compile, not runtime correctness.
- [ ] Native run/test passed when the host matched the target RID; otherwise real-hardware /
      CI validation was recommended.

## Error Handling

**Framework project below 4.8.1 declines modernization** (Pre-Init):
1. Stop that project at the gate — do not emit arm64 fixes it cannot build.
2. State the two viable paths: upgrade to .NET Framework 4.8.1+, or modernize to .NET via
   `dotnet-version-upgrade`. Exclude the project from scope and continue with the rest.

**Target RID does not match a project's OS family**:
1. The tool rejects it per project with a message — surface that to the user.
2. Confirm the correct RID for that project's OS, or exclude the project.

**`Arm64.0004` reports no safe arm64 version** (only a major bump / TFM raise / replacement):
1. Do not auto-apply — present the guided options from the assessment.
2. Let the user choose: accept the major bump, raise the TFM first, or replace the package.

**Cross-compile Gate fails**:
1. Report the unresolved packaging / native-asset / compile issue — do not mark the migration
   complete.
2. Common causes: a package still missing an arm64 native asset (`Arm64.0004`), or an
   unresolved x86 assumption. Return to Execution for that finding.

