Skip to content

Instantly share code, notes, and snippets.

@rmarinho
Created June 3, 2026 15:00
Show Gist options
  • Select an option

  • Save rmarinho/d291e84931720ffffda3b67bd29d5487 to your computer and use it in GitHub Desktop.

Select an option

Save rmarinho/d291e84931720ffffda3b67bd29d5487 to your computer and use it in GitHub Desktop.
maui-coreclr.md
---
name: "maui-coreclr"
description: "End-to-end setup for a .NET MAUI app on CoreCLR (not Mono), iOS Simulator + Android Emulator, with VS Code F5 debugging. Use when the user mentions CoreCLR on iOS, MAUI iOS simulator, vsdbg iOS, breakpoints not hitting in iOS Debug, or setting up MAUI with CoreCLR runtime."
---
# MAUI CoreCLR Debugging Workflow
Use this skill for a .NET MAUI app on CoreCLR with VS Code F5 debugging, especially iOS Simulator CoreCLR and Android emulator/USB debugging.
This guide targets macOS on Apple Silicon (darwin-arm64). Windows and Linux setups require different VSIX platform suffixes, path conventions, and simulator tooling and are not covered here.
Known-good versions from the June 2026 setup:
| Component | Version / source |
| --- | --- |
| .NET SDK | `11.0.100-preview.6.26277.116` |
| Workload set | `11.0.100-preview.4.26261.2` |
| C# extension | `ms-dotnettools.csharp@2.142.16` |
| MAUI extension latest tested main | DevDiv pipeline 18189, `1.16.45-g68ae8b76d9` |
| MAUI extension PR 744644 tested | DevDiv build 14262550, `1.16.47-g61aa27dd03` |
## 1. VS Code extensions
Verify the VS Code CLI is available before running any `code` commands:
```bash
command -v code || echo "Install the shell command via VS Code > Shell Command: Install 'code' command in PATH"
```
Install the C# extension:
```bash
code --install-extension ms-dotnettools.csharp@2.142.16 --pre-release --force
```
### Install latest DevDiv vscode-maui extension from main
The public marketplace extension can be too old for CoreCLR-on-iOS debugger support. Download the internal VSIX from DevDiv pipeline 18189 (`vscode-maui release`):
```bash
VSIX_DIR="${TMPDIR:-/tmp}/vscode-maui-vsix-main"
rm -rf "$VSIX_DIR"
mkdir -p "$VSIX_DIR"
RUN=$(az pipelines runs list --org https://devdiv.visualstudio.com --project DevDiv \
--pipeline-ids 18189 --top 30 --branch main \
--query "[?result=='succeeded'] | sort_by(@, &finishTime) | reverse(@) | [0].id" -o tsv)
az pipelines runs artifact download --org https://devdiv.visualstudio.com --project DevDiv \
--run-id "$RUN" --artifact-name VSIX --path "$VSIX_DIR"
VSIX=$(find "$VSIX_DIR" -name 'dotnet-maui-darwin-arm64-*.vsix' -print -quit)
code --install-extension "$VSIX" --force
```
The VSIX artifact contains all platforms and can be about 1.8 GB. Check disk space first and clean old session VSIX caches if needed. After installing, keep only the platform VSIX you need:
```bash
find "$VSIX_DIR" -type f -name '*.vsix' ! -name 'dotnet-maui-darwin-arm64-*.vsix' -delete
df -h "$HOME"
```
### Install vscode-maui extension from an Azure DevOps PR
Use this when testing a specific `vscode-maui` PR, for example:
`https://dev.azure.com/devdiv/DevDiv/_git/vscode-maui/pullrequest/744644`
1. Get PR metadata and source/merge commit:
```bash
PR_ID=744644
REPO_ID=$(az repos show --org https://devdiv.visualstudio.com --project DevDiv \
--repository vscode-maui --query id -o tsv)
PR_JSON=$(az repos pr show --org https://devdiv.visualstudio.com --id "$PR_ID" -o json)
SOURCE_BRANCH=$(printf '%s' "$PR_JSON" | jq -r '.sourceRefName')
SOURCE_SHA=$(printf '%s' "$PR_JSON" | jq -r '.lastMergeSourceCommit.commitId')
MERGE_SHA=$(printf '%s' "$PR_JSON" | jq -r '.lastMergeCommit.commitId')
printf 'PR %s source=%s sourceSha=%s mergeSha=%s\n' \
"$PR_ID" "$SOURCE_BRANCH" "$SOURCE_SHA" "$MERGE_SHA"
```
If `az repos pr show` cannot resolve the project, open the PR in a browser at `https://dev.azure.com/devdiv/DevDiv/_git/vscode-maui/pullrequest/<PR_ID>` and use the Azure DevOps PR MCP tool (`mcp_azure_devops__pipelines_*`, if available) to retrieve `sourceRefName`, `lastMergeSourceCommit.commitId`, and `lastMergeCommit.commitId`.
2. Find the PR build that produced the VSIX artifact. Prefer a succeeded unofficial build for `refs/pull/<PR_ID>/merge`.
```bash
BUILDS_JSON=$(az rest --method get --uri \
"https://devdiv.visualstudio.com/DevDiv/_apis/build/builds?repositoryId=$REPO_ID&repositoryType=TfsGit&branchName=refs/pull/$PR_ID/merge&queryOrder=queueTimeDescending&api-version=7.1")
BUILD_ID=$(printf '%s' "$BUILDS_JSON" | jq -r '
.value[]
| select((.result == "succeeded" or .result == "partiallySucceeded")
and (.definition.name | test("vscode-maui.*Unofficial|Unofficial Build|vscode-maui"; "i")))
| .id' | head -1)
printf 'Using PR build %s\n' "$BUILD_ID"
```
If `BUILD_ID` is empty, no succeeded unofficial build exists for the PR merge ref. Check the PR pipeline status in Azure DevOps under the Build tab for `refs/pull/<PR_ID>/merge`. You may need to trigger a new build or manually set `BUILD_ID` to a `partiallySucceeded` build ID.
For PR 744644, the working build was:
```text
Build ID: 14262550
Build number: 1.16.47+61aa27dd03
Commit: 61aa27dd03e5702f3291e0664063a02ea3ccb9a2
VSIX: dotnet-maui-darwin-arm64-1.16.47-g61aa27dd03.vsix
```
3. Download and install the PR VSIX:
```bash
VSIX_DIR="${TMPDIR:-/tmp}/vscode-maui-pr-$PR_ID-vsix"
rm -rf "$VSIX_DIR"
mkdir -p "$VSIX_DIR"
az pipelines runs artifact download --org https://devdiv.visualstudio.com --project DevDiv \
--run-id "$BUILD_ID" --artifact-name VSIX --path "$VSIX_DIR"
VSIX=$(find "$VSIX_DIR" -name 'dotnet-maui-darwin-arm64-*.vsix' -print -quit)
code --install-extension "$VSIX" --force
code --list-extensions --show-versions | grep -E 'ms-dotnettools\.(csharp|dotnet-maui|csdevkit)'
```
### Prevent extension auto-update while testing a VSIX
When launching VS Code for a local/internal VSIX test, disable extension auto-update for that VS Code process:
```bash
code --disable-extension-auto-update --reuse-window "$APP_DIR"
```
If the installed `code` CLI does not support that flag, launch normally and immediately verify the extension version:
```bash
code --reuse-window "$APP_DIR"
code --list-extensions --show-versions | grep -E '^ms-dotnettools\.dotnet-maui@'
```
Do not assume the VSIX stayed installed; VS Code may auto-update back to marketplace/latest unless auto-update is disabled for the test session.
## 2. .NET SDK and workload
Install the pinned .NET 11 SDK:
```bash
curl -fsSL https://dot.net/v1/dotnet-install.sh | bash -s -- \
--version 11.0.100-preview.6.26277.116 \
--install-dir "$HOME/.dotnet"
```
Use `~/.dotnet/dotnet` for SDK/workload installation:
```bash
export DOTNET_ROOT="$HOME/.dotnet"
export PATH="$HOME/.dotnet:$PATH"
dotnet workload install ios maui android \
--version 11.0.100-preview.4.26261.2 \
--skip-sign-check
```
If workload install fails with `NU1301` feed errors, complete Section 3 first (create the project and add `NuGet.config`), then re-run the workload install command from the project directory.
Important: the global `maui` DevTools CLI may target .NET 10. If `DOTNET_ROOT="$HOME/.dotnet"` hides the .NET 10 runtime installed under `/usr/local/share/dotnet`, `maui` can fail with "Microsoft.NETCore.App, version 10.0.0 was not found". For `maui` CLI commands, prefer:
```bash
unset DOTNET_ROOT
export PATH="$HOME/.dotnet/tools:/usr/local/share/dotnet:$PATH"
maui version --json
```
## 3. Project setup
Create the app:
```bash
dotnet new maui -n MyApp
cd MyApp
```
Create `global.json` in the project root. Include `sdk.paths` so VS Code's `/usr/local/share/dotnet/dotnet` host can find the pinned SDK installed under `~/.dotnet`.
Correct shape:
```json
{
"sdk": {
"version": "11.0.100-preview.6.26277.116",
"allowPrerelease": true,
"rollForward": "disable",
"paths": [
"$HOME/.dotnet"
]
}
}
```
The `paths` property belongs under `sdk`, not at the JSON root. Replace `$HOME/.dotnet` with the literal expanded path (for example, `/Users/yourname/.dotnet`). JSON does not support shell variables; the actual absolute path is required.
Validate with the same dotnet host VS Code uses:
```bash
cd "$APP_DIR"
/usr/local/share/dotnet/dotnet --version
# expected: 11.0.100-preview.6.26277.116
```
Pin the workload set for the repo:
```bash
dotnet workload update --version 11.0.100-preview.4.26261.2
```
If stale global NuGet feeds break restore with disabled-feed `NU1301` errors, add a project-local `NuGet.config` that clears global sources:
```xml
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="dotnet-public" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json" />
<add key="dotnet9-workloads" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9-workloads/nuget/v3/index.json" />
<add key="xamarin-macios" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-xamarin-xamarin-macios-8354d6d9/nuget/v3/index.json" />
<add key="dotnet-maui" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-dotnet-maui-a33a875e/nuget/v3/index.json" />
</packageSources>
</configuration>
```
## 4. VS Code workspace settings
Use a workspace `.vscode/settings.json`.
Set `dotnet.preferCSharpExtension` to `false` (use C# Dev Kit). This was the tested working configuration for both Android and iOS CoreCLR debugging:
```json
{
"dotnet.preferCSharpExtension": false
}
```
Only set this to `true` if C# Dev Kit is not installed or if the MAUI extension explicitly logs an error recommending the standalone C# extension.
Always relaunch VS Code after changing this setting. Verify the installed extensions:
```bash
code --list-extensions --show-versions | grep -E 'ms-dotnettools\.(csdevkit|csharp|dotnet-maui)'
```
## 5. Required csproj overrides
Edit `MyApp.csproj`. The required overrides target Debug iOS Simulator builds.
```xml
<PublishReadyToRunStripDebugInfo Condition="'$(Configuration)' == 'Debug'">false</PublishReadyToRunStripDebugInfo>
<PublishReadyToRunStripInliningInfo Condition="'$(Configuration)' == 'Debug'">false</PublishReadyToRunStripInliningInfo>
```
Android required no csproj overrides in the tested workflow.
## 6. Android setup and emulator with the `maui` CLI
Use the `maui` DevTools CLI for Android environment and emulator management:
```bash
unset DOTNET_ROOT
export PATH="$HOME/.dotnet/tools:/usr/local/share/dotnet:$PATH"
export ANDROID_HOME="$HOME/Library/Android/sdk"
export ANDROID_SDK_ROOT="$ANDROID_HOME"
export JAVA_HOME="$HOME/Library/Developer/Android/jdk/jdk-21"
export PATH="$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$ANDROID_HOME/cmdline-tools/latest/bin:$JAVA_HOME/bin:$PATH"
maui doctor --json
maui android emulator list --sdk "$ANDROID_HOME" --json
maui device list --platform android --json
```
Start an existing API 35 emulator. If no emulator named `MAUI_android_35_test` exists, ask the user whether to create one before proceeding. Do not create an emulator silently:
```bash
maui android emulator start MAUI_android_35_test --sdk "$ANDROID_HOME" --jdk "$JAVA_HOME"
adb -s emulator-5554 shell getprop sys.boot_completed
maui device list --platform android --json
```
If `maui android emulator start` exits with `E2106`, the message may say stale locks, but the real cause can be low disk space. Check:
```bash
df -h "$HOME"
du -sh "$HOME/.android/avd"/* | sort -h | tail -20
"$ANDROID_HOME/emulator/emulator" -avd <AVD_NAME> -verbose -no-window -no-snapshot-load
```
Do not delete AVDs without confirmation. If deletion is approved, prefer the `maui` CLI:
```bash
maui android emulator delete <AVD_NAME> --sdk "$ANDROID_HOME"
```
## 7. Build and run
### iOS Simulator
Boot a simulator first, then build:
```bash
SIM_UDID=$(xcrun simctl list devices booted -j | python3 -c \
'import json,sys;print(next(d["udid"] for v in json.load(sys.stdin)["devices"].values() for d in v if d["state"]=="Booted"))')
dotnet build MyApp.csproj -c Debug -f net11.0-ios \
-p:RuntimeIdentifier=iossimulator-arm64 \
-p:_DeviceName=":v2:udid=$SIM_UDID"
```
In VS Code press F5 -> Debug iOS / Simulator. The MAUI extension copies `libvsdbgremotecoreclrtarget.dylib` into the `.app` and launches with the required `CORECLR_*` environment variables. Set a breakpoint in `MainPage.xaml.cs` inside `OnCounterClicked` and click the button in the simulator.
### Android Emulator
When multiple Android devices are connected, always target the desired emulator explicitly. VS Code passes `AdbTarget` URL-encoded:
```bash
/usr/local/share/dotnet/dotnet build -t:Run \
-p:Configuration=Debug \
-f net11.0-android \
-p:AdbTarget=-s%20emulator-5554 \
-p:AndroidAttachDebugger=true \
-p:AndroidSdkDirectory="$ANDROID_HOME" \
-p:JavaSdkDirectory="$JAVA_HOME" \
MyApp.csproj
```
Validate install/launch:
```bash
adb -s emulator-5554 shell pm list packages com.companyname.myapp
adb -s emulator-5554 shell pidof com.companyname.myapp
```
In VS Code press F5 -> Debug Android / Emulator. Set a breakpoint in `MainPage.xaml.cs` inside `OnCounterClicked` and click the button in the emulator.
## 8. Launch VS Code with the correct setup
Launch from the project directory so `global.json` is discovered and `sdk.paths` is applied.
Do not export `DOTNET_ROOT` when launching VS Code; the `global.json` `sdk.paths` mechanism handles SDK resolution. Only set `DOTNET_ROOT` when running `dotnet workload` or `dotnet restore` commands directly.
```bash
APP_DIR="/path/to/MyApp"
unset DOTNET_ROOT
export PATH="$HOME/.dotnet:$HOME/.dotnet/tools:/usr/local/share/dotnet:$PATH"
export ANDROID_HOME="$HOME/Library/Android/sdk"
export ANDROID_SDK_ROOT="$ANDROID_HOME"
export JAVA_HOME="$HOME/Library/Developer/Android/jdk/jdk-21"
export PATH="$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$ANDROID_HOME/cmdline-tools/latest/bin:$JAVA_HOME/bin:$PATH"
cd "$APP_DIR"
/usr/local/share/dotnet/dotnet --version
code --list-extensions --show-versions | grep -E 'ms-dotnettools\.(csdevkit|csharp|dotnet-maui)'
code --disable-extension-auto-update --reuse-window .
```
If `code --disable-extension-auto-update` is unavailable, use `code --reuse-window .` and re-check the MAUI extension version after launch.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment