Development Guide

Updated: 2026-08-27 19:16 ET · Audited: 2026-06-26

TL;DR: Local setup and day-to-day development workflows for ALPA Mobile: prerequisites, restore/build commands (dotnet build ALPAMobile/ALPAMobile.csproj -f net10.0-ios|-android), live debugging with MauiDevFlow, pre-commit git hooks (activate once with git config core.hooksPath .githooks), and troubleshooting. Versions are centralized in global.json and Directory.Build.props — read Version management before touching any version number.

This guide covers local setup and day-to-day development workflows for ALPA Mobile.

Table of contents

Prerequisites

Install MAUI workloads:

dotnet workload install maui
The workload version is pinned via tools.dotnet-workload-version in global.json. No --version flag needed — it reads from the file automatically.

Verify SDK:

dotnet --version

Repository layout

The solution (ALPAMobile.sln) is split into n-tier projects; dependency direction is enforced by architecture tests (UnitTest/Architecture/LayerDependencyRulesTests.cs).

Restore and build

# Restore
dotnet restore ALPAMobile.sln

# iOS debug build
dotnet build ALPAMobile/ALPAMobile.csproj -f net10.0-ios -c Debug

# Android debug build
dotnet build ALPAMobile/ALPAMobile.csproj -f net10.0-android -c Debug

Run the app

Use your normal IDE workflow for deploying to simulator/device.

# iOS debug build
dotnet build ALPAMobile/ALPAMobile.csproj -f net10.0-ios -c Debug

# Android debug build
dotnet build ALPAMobile/ALPAMobile.csproj -f net10.0-android -c Debug

Live debugging with MauiDevFlow

MauiDevFlow enables live UI inspection, screenshots, tapping, log streaming, and network monitoring against a running simulator or emulator — no IDE required.

Prerequisites

dotnet tool install --global Redth.MauiDevFlow.CLI
dotnet tool install --global appledev.tools   # iOS / Mac Catalyst
dotnet tool install --global androidsdk.tool  # Android

# Keep the MauiDevFlow skill definition up to date
maui-devflow update-skill

Run the app for MauiDevFlow

The Redth.MauiDevFlow.Agent package is already included in the project and registered in MauiProgram.cs under #if DEBUG. Build and launch normally — the agent starts automatically on debug builds.

iOS Simulator:

dotnet build ALPAMobile/ALPAMobile.csproj -f net10.0-ios
dotnet build ALPAMobile/ALPAMobile.csproj -f net10.0-ios -t:Run \
  -p:_DeviceName=:v2:udid=<SIMULATOR_UDID>

# Find your simulator UDID
xcrun simctl list devices booted

Android Emulator:

dotnet build ALPAMobile/ALPAMobile.csproj -f net10.0-android -t:Run

Connect and inspect

maui-devflow wait          # prints the port when connected
maui-devflow MAUI status   # confirm connection
maui-devflow MAUI screenshot --output screen.png   # capture screen
maui-devflow MAUI tree --depth 15                  # visual element tree
maui-devflow MAUI logs --follow                    # stream live logs
maui-devflow MAUI network                          # monitor HTTP requests
maui-devflow MAUI tap --automationId "MyButton"    # interact with UI

Verifying plain-web UI with Playwright

MauiDevFlow targets the MAUI app shell — it needs the Redth.MauiDevFlow.Agent registered in MauiProgram.cs and a running simulator/emulator to attach to. Plain-web Blazor Server surfaces with no MAUI host at all — e.g. PreviewHost (its own repo, ALPAMobile.PreviewHost, since AB#2309), the admin-portal WYSIWYG preview seed — have no agent to attach to, so MauiDevFlow doesn't apply. Playwright fills the equivalent role there: drive a real browser engine against the running dotnet run server and inspect/interact with it programmatically.

Why this matters, not just "nice to have": curl can fetch served HTML/CSS and confirm a build's output bytes are correct, but it cannot execute JavaScript or simulate pointer, drag, or wheel events at all. Any interactive behavior — carousel drag-to-scroll, scroll chaining, drag-and-drop — is invisible to curl no matter how carefully you inspect the markup. A real "it works" claim for that class of bug requires actually driving a browser.

Setup

# Playwright itself is available via npx — no persistent install needed
npx --yes playwright install chromium   # first run only, per machine

# WebKit — the closest available engine to Safari when Chromium-only
# testing isn't enough to explain a Safari-specific report
npx --yes playwright install webkit

Driving the running server

Write a small throwaway .mjs script (scratchpad directory, not committed) and run it with node against the already-running dotnet run instance:

import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 500, height: 950 } });
await page.goto('http://localhost:5311/home', { waitUntil: 'networkidle' });

// Real pointer drag — mouse.move/down/move/up, not a single jump —
// is what actually exercises mousemove-driven drag handlers.
const box = await (await page.$('.alpa-feed-track')).boundingBox();
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await page.mouse.move(box.x + box.width / 2 - 150, box.y + box.height / 2, { steps: 10 });
await page.mouse.up();

// Real wheel event, for scroll-chaining bugs mouse-drag can't surface
await page.mouse.wheel(0, 300);
await page.waitForTimeout(150); // see gotcha below — read too early and you'll see stale state

Gotchas hit in practice

Common workflows

# Add or update dependencies
dotnet restore

# Build all solution projects
dotnet build ALPAMobile.sln

After feature changes:

Git Hooks

Pre-commit hooks enforce code-quality gates. Activate once per clone / worktree:

git config core.hooksPath .githooks

Hooks run in order at commit time:

#CheckTriggers onBypass variable
1Doc timestamp disciplinestaged docs/component-specifications/*.htmlSKIP_TIMESTAMP_CHECK=1
2Secret / credential scanall staged filesSKIP_SECRET_SCAN=1
3Markdown lintstaged .md filesSKIP_MD_LINT=1
4N-Tier architecture testsstaged .cs / .razorSKIP_ARCH_CHECK=1
5Full unit test suitestaged .cs / .razorSKIP_UNIT_TESTS=1
6Video-asset placement — staged video files (.mp4/.mov/…) must live under docs/ (documentation, e.g. walkthrough replay clips) so no csproj glob can ship them in the app bundlestaged video filesSKIP_MEDIA_CHECK=1

Bypass variables may be combined. Document the reason in the commit message when bypassing. Build smoke is handled by CI — not duplicated in the hook.

Manual timestamp validation (outside of a commit):

bash docs/validate-doc-timestamps.sh          # staged files only
bash docs/validate-doc-timestamps.sh --all    # full spec corpus

Troubleshooting

Android SDK not found

export ANDROID_HOME="$HOME/Library/Android/sdk"
export PATH="$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/tools"

MAUI workload issues

dotnet workload repair

NuGet restore issues

dotnet nuget locals all --clear
dotnet restore ALPAMobile.sln

VS Code "maui: Build" task exits 155

VS Code's maui: Build pre-launch task reports a bare exit code 155 with no further detail. This is MSBuild failing SDK resolution before it reaches any target — always reproduce with a direct dotnet build in a terminal first, which surfaces the real error VS Code swallows:

dotnet --version   # if this alone fails to resolve, the SDK pinned in global.json isn't installed

If dotnet --version reports "Requested SDK version … not found", the sdk.version pinned in global.json (rollForward: disable — exact match required) isn't installed on this machine. Install it — see Installing a pinned SDK with dotnetup — or, if the pin itself is wrong (e.g. a merge/rebase brought in a version nobody has installed), point global.json at a version the team actually has and follow To upgrade stable MAUI to keep Directory.Build.props in sync.

iOS code signing: "key not found in keychain" on a simulator build

Symptom: error: iOS code signing key '…' not found in keychain on dotnet build -f net10.0-ios -r iossimulator-arm64 -c Debug.

This should never happen for a simulator build. Per Microsoft's .NET for iOS build properties and manual-provisioning docs, code signing applies only to physical-device builds — the simulator needs no certificate or provisioning profile at all, and if CodesignKey/CodesignProvision are left unset, the build auto-selects whatever's appropriate.

If this error appears on a simulator build, the shared ALPAMobile/ALPAMobile.csproj has a CodesignKey scoped too broadly — e.g. by TargetFramework + Configuration alone, with no RuntimeIdentifier check, so it also fires for iossimulator-*. The convention going forward:

Version management

MAUI, SDK, and workload versions are centralized — do not hardcode them in pipeline YMLs or project files.

FileControls
global.json.NET SDK version + workload-set version
Directory.Build.props$(MauiVersion) — the Microsoft.Maui.Controls NuGet package version

To upgrade stable MAUI

  1. Update sdk.version and tools.dotnet-workload-version in global.json
  2. Update <MauiVersion> in Directory.Build.props
  3. Commit — pipelines and all project files pick it up automatically
SDK ↔ workload coupling: The SDK version and workload-set version must stay on the same feature band (e.g. SDK 10.0.101 ↔ workload-set 10.0.101).
NuGet package version ≠ SDK/workload version: Microsoft.Maui.Controls NuGet packages follow a separate version sequence (e.g. 10.0.51) that does not match the SDK version (10.0.101). Always verify the NuGet package version exists on nuget.org before setting it in Directory.Build.props.

To test a MAUI nightly build

  1. Go to Pipelines → Run pipeline → Advanced options
  2. Set useMauiNightly = true
  3. Paste the nightly version string into mauiNightlyVersion (e.g. 10.0.60-ci.main.26203.10)
  4. Browse available nightly versions at the dotnet10 package feed

This overrides only the NuGet package version at build time — the SDK and workload-set stay on the stable version in global.json.

Installing a pinned SDK with dotnetup

Updating global.json's sdk.version/tools.dotnet-workload-version (see To upgrade stable MAUI) only changes what the repo expects — it does not install anything. rollForward: disable means an exact match is required, so every machine that builds this repo needs the matching SDK actually installed before dotnet build will resolve at all.

This repo's dev machines manage .NET installs with dotnetup (user-level installs, multiple SDK bands side by side — not the system-wide installer). To install a new pinned SDK band and bring its MAUI workloads up to date:

# Install the SDK band pinned in global.json (repeat per new version bump)
dotnetup sdk install <sdk.version> --set-default-install

# Update/install the matching MAUI workloads for that band
dotnet workload update --version <tools.dotnet-workload-version>

Example, upgrading to SDK 10.0.400:

dotnetup sdk install 10.0.400 --set-default-install
dotnet workload update --version 10.0.400.1
--set-default-install updates PATH/DOTNET_ROOT so the new band becomes the default dotnet on this machine — required for global.json resolution to find it. If dotnetup or dotnet aren't yet on PATH (fresh machine, before first dotnetup init), run them via the dotnetup-managed absolute paths instead: ~/.dotnetup/dotnetup sdk install … and ~/Library/Application\ Support/dotnetup/dotnet/dotnet workload update … (macOS; installed under ~/.dotnetup and dotnetup's managed dotnet directory).

If a dotnet workload update reports nothing to update because no workloads are installed at all for that SDK band yet, restore them from the project instead — this reads the project's target frameworks and installs exactly the workloads it needs:

dotnet workload restore ALPAMobile/ALPAMobile.csproj