.NET MAUI  

.NET MAUI CoreCLR Migration: Finding Mobile Regressions

.NET MAUI applications targeting mobile platforms are moving to a unified runtime model.

Starting with .NET 11 Preview 4, CoreCLR became the default runtime for .NET MAUI applications targeting Android, iOS, and Mac Catalyst. With .NET 11 Preview 6, CoreCLR became the only runtime for those mobile targets, and the previous mechanism for selecting Mono was removed. Blazor WebAssembly remains on Mono and is not affected by this transition.

For developers, this is more than a project-file change.

A runtime migration can affect:

  • Application startup

  • Memory behavior

  • CPU usage

  • Threading

  • Native interop

  • Reflection

  • Trimming

  • AOT behavior

  • Debugging

  • Profiling

  • Third-party libraries

  • Platform-specific functionality

Microsoft's stated direction is to unify .NET MAUI mobile applications around CoreCLR, with expected benefits across debugging, profiling, Hot Reload, application size, and performance.

But a runtime-level architectural change should not be evaluated only by whether the application launches.

The more useful question is:

How do you systematically find mobile regressions after moving a .NET MAUI application from the previous runtime path to CoreCLR?

This article presents a practical regression-testing strategy.

What Changed in .NET MAUI

Historically, .NET MAUI mobile applications used Mono for Android, iOS, and Mac Catalyst.

The transition introduces:

Previous model

.NET MAUI
   |
   +-- Android → Mono
   +-- iOS → Mono
   +-- Mac Catalyst → Mono

The .NET 11 model is:

.NET MAUI
   |
   +-- Android → CoreCLR
   +-- iOS → CoreCLR
   +-- Mac Catalyst → CoreCLR

As of .NET 11 Preview 6, developers no longer select between Mono and CoreCLR for these mobile targets. The previous Mono-selection build property has been removed.

This makes regression testing particularly important.

Why Runtime Migration Can Cause Regressions

A .NET MAUI application contains several layers:

Application Code
      |
      v
.NET MAUI
      |
      v
.NET Runtime
      |
      v
Platform Bindings
      |
      v
Android / iOS / Mac Catalyst
      |
      v
Native OS APIs

A runtime change can expose problems at any layer.

For example:

Application
   ↓
Reflection
   ↓
Runtime behavior
   ↓
Native binding

A feature that worked under the previous runtime may behave differently after migration.

This does not mean CoreCLR is inherently less compatible.

It means runtime migrations should be tested as architectural changes rather than ordinary dependency upgrades.

Start With a Baseline

Before changing the target framework, record the current application's behavior.

For example:

MetricExisting RuntimeCoreCLR
Cold startupMeasureMeasure
Warm startupMeasureMeasure
Memory after launchMeasureMeasure
Peak memoryMeasureMeasure
CPU during startupMeasureMeasure
App sizeMeasureMeasure
Screen transitionMeasureMeasure
API request latencyMeasureMeasure
Crash rateMeasureMeasure
ANR/hang rateMeasureMeasure

The values should be collected from the actual application and target devices.

Do not publish assumed improvements.

The purpose of the baseline is to identify behavioral differences after migration.

Upgrade the Target Framework

A .NET MAUI project targeting .NET 11 will use CoreCLR for the mobile platforms.

For example:

<PropertyGroup>
    <TargetFrameworks>
        net11.0-android;
        net11.0-ios;
        net11.0-maccatalyst
    </TargetFrameworks>

    <UseMaui>true</UseMaui>
</PropertyGroup>

The exact project configuration depends on the existing application.

After upgrading, verify:

dotnet --info

and confirm that the expected .NET 11 SDK is being used.

Install or update the MAUI workload as required by the SDK:

dotnet workload install maui

Microsoft's CoreCLR migration guidance specifically recommends installing the .NET 11 Preview 6 SDK and the MAUI workload, then running existing applications through a validation checklist.

Do Not Treat Compilation as Migration Success

A successful build proves only that the project can be compiled.

It does not prove:

Application starts
        ↓
Navigation works
        ↓
Native APIs work
        ↓
Authentication works
        ↓
Storage works
        ↓
Networking works
        ↓
Background operations work
        ↓
App survives suspend/resume

A mobile runtime migration requires behavioral validation.

Create a Smoke-Test Matrix

Start with a short smoke-test suite.

AreaTest
LaunchCold start
NavigationOpen every primary screen
AuthenticationLogin/logout
NetworkingAPI request
StorageRead/write local data
PermissionsRequest required permissions
CameraCapture image
LocationAccess location
NotificationsReceive notification
Deep linksOpen application through link
BackgroundingSuspend and resume
RotationChange orientation where supported
Offline modeDisable network
LogoutClear session
UpgradeUpgrade existing installation

The exact tests depend on the application.

The goal is to catch obvious runtime incompatibilities before performance testing.

Test Android, iOS, and Mac Catalyst Separately

Do not assume:

Android works

means:

iOS works

or:

Mac Catalyst works

The runtime is unified, but the native platform layers remain different.

Use separate test matrices:

Android
   |
   +-- Device A
   +-- Device B
   +-- Device C

iOS
   |
   +-- Device A
   +-- Device B
   +-- Device C

Mac Catalyst
   |
   +-- macOS configuration

At minimum, test the platforms your application actually ships.

Test Different Device Classes

A runtime migration can behave differently depending on device capability.

Test representative:

Low-memory device
Mid-range device
High-end device

The exact devices should be selected based on your supported-device policy.

Record:

  • Startup time

  • Memory

  • CPU

  • Frame responsiveness

  • Crashes

  • ANRs/hangs

  • Battery behavior

Avoid drawing conclusions from a single high-end development machine.

Measure Cold Startup

Startup is one of the first metrics worth measuring.

A basic measurement should distinguish:

Process launch
     ↓
Runtime initialization
     ↓
MAUI initialization
     ↓
Application initialization
     ↓
First usable screen

Define a consistent endpoint.

For example:

Cold startup =
launch command
→ first interactive application screen

Do not compare two runtimes using different definitions of "startup."

Measure Warm Startup

Also test:

Application backgrounded
        ↓
Application resumed

Warm startup can reveal issues that are invisible during a cold launch.

For example:

  • State restoration problems

  • Resource lifecycle problems

  • Native handler issues

  • Timer behavior

  • Event subscription problems

A migration test should therefore contain both cold and warm scenarios.

Watch Memory Usage

A runtime migration can change memory characteristics.

Measure at consistent points:

After launch
After navigation
After loading data
After opening media
After repeated navigation
After background/resume

For example:

Test StageBaselineCoreCLR
LaunchMeasureMeasure
LoginMeasureMeasure
Main screenMeasureMeasure
Large listMeasureMeasure
Image screenMeasureMeasure
After navigation loopMeasureMeasure

The important signal is not merely peak memory.

Look for unexpected growth:

Screen A
 ↓
Screen B
 ↓
Screen C
 ↓
Screen A
 ↓
Screen B

If memory continually increases after returning to previously visited screens, investigate possible application-level leaks before attributing the issue to CoreCLR.

Test Navigation Loops

A practical memory regression test is repeated navigation.

For example:

Home
 ↓
Details
 ↓
Edit
 ↓
Details
 ↓
Home

Repeat this sequence many times.

Monitor memory.

This can expose:

  • Event-handler leaks

  • Static references

  • Unreleased native resources

  • Image retention

  • Handler lifecycle problems

  • Application-level object retention

The runtime migration may change the symptoms without necessarily being the underlying cause.

Test Native Interop

Mobile applications frequently interact with native APIs.

Examples include:

Camera
Location
Bluetooth
Push notifications
Keychain / secure storage
Sensors
Media
Files
Contacts
Platform-specific UI

Test every native integration.

A runtime migration can expose assumptions in bindings or interop code.

For example:

#if ANDROID
// Android-specific implementation
#endif

#if IOS
// iOS-specific implementation
#endif

Do not assume that shared C# code is the only relevant compatibility surface.

Review Reflection

Reflection-heavy code deserves special attention.

Search the application for:

Assembly.Load
Type.GetType
GetMethod
GetProperty
Activator.CreateInstance
MakeGenericType
reflection-based serializers
dynamic invocation

Reflection may be especially important when combined with trimming or AOT-related deployment configurations.

The goal is not to remove reflection automatically.

Instead, identify where runtime discovery is essential and verify those paths explicitly.

Test Dependency Injection

A typical MAUI application may register services in MauiProgram:

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder =
            MauiApp.CreateBuilder();

        builder
            .UseMauiApp<App>();

        builder.Services.AddSingleton<
            IAuthService,
            AuthService>();

        builder.Services.AddTransient<
            MainPage>();

        return builder.Build();
    }
}

After migration, verify:

Service registration
 ↓
Resolution
 ↓
Constructor injection
 ↓
Page creation

Test startup paths that create many services.

A runtime migration should not be considered complete merely because the first page loads.

Test Async Operations

Mobile applications frequently perform:

API calls
Database access
File operations
Image processing
Background synchronization

Test both success and failure.

For example:

public async Task LoadAsync(
    CancellationToken cancellationToken)
{
    var response =
        await httpClient.GetAsync(
            "api/data",
            cancellationToken);

    response.EnsureSuccessStatusCode();
}

Validate:

Success
Timeout
Cancellation
Offline mode
Server error
App backgrounding
App resume

This is especially important because mobile applications can lose connectivity or be suspended while an asynchronous operation is active.

Test App Suspension and Resume

A mobile runtime test should include:

Open app
   ↓
Start operation
   ↓
Background app
   ↓
Resume app
   ↓
Validate state

Test operations such as:

API request
File upload
Database transaction
Authentication flow
Media processing

The application should handle lifecycle transitions intentionally.

Do not assume an async operation that works on desktop behaves identically during mobile suspension.

Test Background Work

Identify all background work:

Timers
Tasks
Periodic synchronization
Push notification handlers
Background services
Event subscriptions

Then verify:

Start
Stop
Suspend
Resume
Terminate
Restart

A migration regression may appear as:

Background operation
      ↓
App suspended
      ↓
Unexpected exception
      ↓
State lost

The exact behavior depends on the mobile platform and application architecture.

Test Secure Storage and Local Storage

Storage should be part of the regression suite.

Test:

Write
Read
Update
Delete
Clear
Upgrade
Restart

For example:

await SecureStorage.Default
    .SetAsync("access_token", token);

var stored =
    await SecureStorage.Default
        .GetAsync("access_token");

Verify that values remain available across:

Application restart
Background/resume
Upgrade

where the application's requirements expect persistence.

Test Authentication Flows

Authentication often combines:

Web authentication
Browser
Redirect/deep link
Token storage
HTTP requests
Refresh
Logout

Test the complete flow after migration.

For example:

Login
 ↓
Browser
 ↓
Redirect
 ↓
Callback
 ↓
Token
 ↓
API
 ↓
Logout

Do not test only the initial login screen.

Runtime or platform integration issues can occur at the callback or native-browser boundary.

Test Deep Links

If the application supports:

myapp://orders/123

or universal/app links, test:

App closed
App backgrounded
App already open
Invalid link
Authenticated user
Unauthenticated user

Verify that navigation reaches the expected page.

Deep-link handling is a useful regression test because it crosses application and platform boundaries.

Test Push Notifications

If the application uses push notifications, validate:

Permission request
Token registration
Foreground notification
Background notification
Notification tap
Deep-link navigation
Cold-start notification

A runtime migration should be tested against the entire lifecycle rather than only notification receipt.

Test UI Responsiveness

A runtime change can affect perceived application performance even when API latency is unchanged.

Measure scenarios such as:

Open page
Scroll large list
Load images
Navigate between pages
Open modal
Close modal
Perform search

Record observable metrics where your tooling supports them.

At minimum, look for:

Frame drops
Input lag
Visible freezes
Long UI-thread blocks

A page that technically loads successfully can still be a regression if it becomes visibly less responsive.

Separate UI Thread From Background Work

Check for synchronous work on the UI thread.

For example:

var data =
    VeryExpensiveOperation();

If this executes during a page lifecycle event, it can block rendering.

Prefer an asynchronous approach when appropriate:

var data =
    await VeryExpensiveOperationAsync();

But do not automatically move CPU-heavy work to Task.Run.

Measure the actual workload and ensure that UI-affecting operations are scheduled appropriately for the platform.

Test Native Libraries

Inventory native dependencies:

Android libraries
iOS frameworks
NuGet packages with native assets
Binding libraries
Graphics libraries
Analytics SDKs
Authentication SDKs

For each dependency, verify:

Build
Package
Launch
Initialization
Runtime usage
Release build

A library that works in Debug may still fail in a release or trimmed build.

Test Release Builds

Debug builds are not sufficient.

Test:

Debug
Release

and the actual deployment configuration.

The release configuration can change:

  • Linking

  • Trimming

  • AOT behavior

  • Optimization

  • Native packaging

  • Resource handling

Microsoft's Native AOT documentation notes that AOT deployments introduce restrictions around dynamic loading and runtime code generation and rely on trimming analysis.

If your application uses AOT or aggressive trimming, these scenarios deserve dedicated testing.

Test Trimming-Sensitive Code

Search for code that depends on runtime discovery:

Reflection
Dynamic activation
Serialization
Plugin loading
Assembly scanning

Then run the release configuration and verify those paths.

A feature that works in Debug but fails after trimming should be treated as a deployment compatibility problem.

Test Third-Party Libraries

Create a dependency inventory:

DependencyPurposeAndroidiOSMac CatalystRelease
Package AAnalyticsTestTestTestTest
Package BAuthenticationTestTestTestTest
Package CStorageTestTestTestTest

Do not assume a NuGet package is compatible merely because the project compiles.

Verify actual runtime behavior.

Compare Application Size

Microsoft identifies application size as one area expected to benefit from the unified CoreCLR direction.

Measure actual output size instead of assuming an improvement.

Record:

APK size
AAB size
IPA size
Installed application size

where applicable to your distribution process.

Keep packaging settings identical between comparison builds.

Compare Startup and Memory Together

A useful regression report should combine multiple dimensions.

For example:

MetricPreviousCoreCLRDifference
Cold startupMeasureMeasureCalculate
Warm startupMeasureMeasureCalculate
Memory after launchMeasureMeasureCalculate
Peak memoryMeasureMeasureCalculate
Package sizeMeasureMeasureCalculate
API latencyMeasureMeasureCalculate

Do not optimize one metric while ignoring another.

A smaller package with higher memory usage may be a different trade-off from a faster startup with larger storage requirements.

Create a Regression Test Matrix

A practical matrix can be:

Platform
 ├── Android
 │    ├── Debug
 │    └── Release
 │
 ├── iOS
 │    ├── Debug
 │    └── Release
 │
 └── Mac Catalyst
      ├── Debug
      └── Release

For each configuration:

Launch
Navigation
Authentication
Networking
Storage
Native APIs
Background/resume
Deep links
Notifications
Performance
Memory

This converts an ambiguous migration into a repeatable test plan.

Automate the Smoke Tests

Automated UI testing can cover stable flows.

For example:

Launch application
    ↓
Login
    ↓
Open dashboard
    ↓
Open details
    ↓
Navigate back
    ↓
Logout

The exact automation framework depends on your application and platform.

The goal is to run the same critical flow against the old and new runtime configurations.

Automated smoke tests are especially valuable when the migration affects multiple platform targets.

Build a Before-and-After Performance Harness

For performance-sensitive applications, create a standard test scenario.

Example:

Scenario: Open Customer Dashboard

1. Launch application.
2. Authenticate.
3. Open dashboard.
4. Load 100 records.
5. Scroll through list.
6. Open detail screen.
7. Return to dashboard.
8. Repeat five times.

Measure:

Startup
API latency
Rendering responsiveness
Memory
CPU
Crash count

Run the same scenario against both runtime versions.

This is much more useful than testing random screens manually.

Classify Regressions

Not every difference is a runtime bug.

Use categories:

CategoryExample
RuntimeChanged execution behavior
MAUIFramework behavior
Native platformAndroid/iOS behavior
DependencyThird-party library
ApplicationExisting application bug exposed
ConfigurationBuild or deployment setting
ToolingSDK/workload issue

This prevents every migration problem from being incorrectly attributed to CoreCLR.

Create a Minimal Reproduction

If a problem appears after migration, reduce it.

For example:

Full application
      ↓
Remove unrelated pages
      ↓
Remove networking
      ↓
Remove database
      ↓
Keep failing native operation
      ↓
Create minimal MAUI project

A small reproduction makes it easier to determine whether the problem is:

MAUI
CoreCLR
Native platform
Third-party library
Application code

Microsoft is specifically asking developers to test applications during the .NET 11 preview period so migration feedback can influence work before general availability.

Common Regression Patterns

Application Launches but Crashes Later

Check:

  • Lazy initialization

  • Reflection

  • Native libraries

  • Dependency injection

  • Platform-specific code

Debug Works but Release Fails

Investigate:

  • Trimming

  • AOT

  • Reflection

  • Native packaging

  • Conditional compilation

Android Works but iOS Fails

Inspect:

  • Platform-specific bindings

  • Native frameworks

  • Entitlements

  • Linker/trimming behavior

  • iOS-specific lifecycle handling

Memory Increases After Navigation

Use a repeated navigation test and investigate retained references before concluding that the runtime is responsible.

Startup Is Slower

First separate:

Runtime initialization
MAUI initialization
Application initialization
Network initialization
Database initialization

Without this separation, "startup is slower" is difficult to diagnose.

Troubleshooting

The Project Still References Mono Settings

Review the project file for old runtime-selection properties.

As of .NET 11 Preview 6, the previous build property used to select Mono for MAUI mobile targets has been removed because CoreCLR is now the only runtime path.

The Application Builds but Does Not Start

Start with:

dotnet clean
dotnet restore
dotnet build

Then verify:

dotnet --info

and confirm the correct SDK/workload installation.

Also test a minimal MAUI project targeting the same platform.

A Native Library Stops Working

Verify:

  1. Package version.

  2. Native binary architecture.

  3. Platform support.

  4. Initialization sequence.

  5. Release configuration.

  6. Trimming/AOT requirements.

Reflection-Based Code Fails

Search for runtime-discovered types and test them explicitly under the release configuration.

If trimming or AOT is involved, inspect the relevant analyzer warnings rather than suppressing them blindly.

Performance Changed but There Is No Crash

This is still a regression candidate.

Measure:

Startup
Memory
CPU
UI responsiveness
Async operations
API latency

Then compare against the baseline.

Best Practices

  1. Record a baseline before migration.

  2. Test Android, iOS, and Mac Catalyst independently.

  3. Test both Debug and Release configurations.

  4. Test representative physical devices.

  5. Measure startup and memory rather than relying on subjective impressions.

  6. Test all native integrations.

  7. Review reflection-heavy code.

  8. Test dependency compatibility.

  9. Test background/resume behavior.

  10. Test authentication and deep links.

  11. Test push notifications.

  12. Run repeated navigation tests for memory leaks.

  13. Test release builds with trimming/AOT configurations used by the application.

  14. Keep application and dependency changes controlled during regression analysis.

  15. Build minimal reproductions for confirmed runtime issues.

  16. Record the exact SDK, workload, platform version, device, and build configuration.

Frequently Asked Questions

Is CoreCLR now the only runtime for .NET MAUI mobile apps?

For .NET 11 Preview 6, CoreCLR is the only runtime for .NET MAUI applications targeting Android, iOS, and Mac Catalyst. The previous Mono runtime-selection path was removed. Blazor WebAssembly continues to use Mono.

Do I need to manually configure CoreCLR?

For .NET 11 MAUI mobile targets, CoreCLR is the runtime path. The older project configuration used to select Mono is no longer the model in Preview 6.

Does moving to CoreCLR guarantee better performance?

No.

Microsoft describes benefits including performance, debugging, profiling, Hot Reload, and application size as goals of the unified runtime direction, but application-level performance must be measured against the actual workload.

Should I test only Android?

No.

Android, iOS, and Mac Catalyst are separate deployment targets with different native platform integrations. Test every platform your application supports.

What should I measure first?

Start with:

Cold startup
Warm startup
Memory
CPU
UI responsiveness
API latency
Crash rate
Package size

Then investigate platform-specific functionality.

Can existing MAUI applications require code changes?

Potentially.

The official migration guidance asks developers to build existing applications against Preview 6 and report compatibility problems. The exact changes depend on the application's APIs, dependencies, native integrations, and runtime assumptions.

Does this change affect Blazor WebAssembly?

No. Microsoft explicitly states that Blazor WebAssembly continues to use Mono and is not affected by the .NET 11 MAUI CoreCLR transition.

Conclusion

The move to CoreCLR represents a significant runtime change for .NET MAUI mobile applications.

As of .NET 11 Preview 6, Android, iOS, and Mac Catalyst applications use CoreCLR exclusively, replacing the previous runtime-selection model involving Mono. Microsoft is encouraging developers to test their existing applications during the preview period so compatibility issues can be identified before general availability.

The correct migration strategy is not:

Update TargetFramework
        ↓
Build succeeds
        ↓
Migration complete

Instead, use:

Baseline
   ↓
Upgrade
   ↓
Smoke Test
   ↓
Platform Test
   ↓
Release Test
   ↓
Performance Test
   ↓
Memory Test
   ↓
Native Integration Test
   ↓
Dependency Test
   ↓
Regression Analysis

The most important lesson is that a runtime migration should be evaluated as a behavioral compatibility project, not merely a compilation exercise.

Measure startup, memory, CPU, responsiveness, package size, native integrations, asynchronous operations, lifecycle behavior, and release builds.

When a regression appears, isolate it before assigning blame:

Application
     ↓
.NET MAUI
     ↓
CoreCLR
     ↓
Native Platform
     ↓
Third-Party Dependency

That process gives teams a defensible way to determine whether a problem is caused by the runtime migration, an existing application assumption, a native platform integration, or a third-party dependency.

For applications moving toward .NET 11, the preview period is therefore not just a chance to try the new runtime. It is the appropriate time to establish a regression baseline, exercise real applications, and identify compatibility issues while the runtime transition is still being validated.