Migrating a small Xamarin.Forms application to .NET MAUI is usually manageable.
Migrating a large enterprise application is a different story.
The difficulty is rarely the Xamarin.Forms namespace replacement or converting a .csproj file. The real challenge is that an enterprise application has accumulated years of assumptions around Xamarin, third-party libraries, native platform APIs, custom renderers, background processing, authentication, local databases, CI/CD, and production behavior.
Microsoft provides migration tooling and official migration paths, but the tooling should be treated as an accelerator—not as the migration strategy itself. The Upgrade Assistant can perform many project and namespace transformations, but additional manual work is expected.
The practical question therefore becomes:
How do you migrate a large Xamarin application while continuing to develop, test, and release the product?
This article focuses on that problem.
1. Don't Start With the .csproj
One of the most common mistakes is starting the migration by changing:
<TargetFramework>...</TargetFramework>
and then fixing whatever breaks.
That approach works reasonably well for smaller applications.
For a large application, it creates a huge error list before you understand where the real risks are.
Instead, start by answering:
- How many screens exist?
- Which screens are business-critical?
- Which NuGet packages are Xamarin-specific?
- Which controls use custom renderers?
- Which features depend on native Android/iOS code?
- Are there background services?
- How are push notifications implemented?
- How does offline synchronization work?
- Which parts depend on third-party SDKs?
- How is the application signed and distributed?
- Which production workflows cannot fail?
The first deliverable should be a dependency and feature map, not a converted project.
2. Divide the Application Into Migration Units
A 200-screen application should not be treated as one migration task.
Break it into units.
For example:
Application
│
├── Authentication
├── Customer
│ ├── Customer Search
│ ├── Customer Details
│ └── Customer History
│
├── Orders
│ ├── Order Creation
│ ├── Order Editing
│ └── Order Submission
│
├── Offline
│ ├── Local Database
│ ├── Queue
│ └── Synchronization
│
├── Notifications
├── Location
├── Documents
└── Reporting
Each unit should have its own migration status.
A simple tracking model is enough:
| Feature | Xamarin | MAUI | Dependency Risk | Testing |
|---|---|---|---|---|
| Login | Working | Migrated | Low | Automated |
| Customer Search | Working | Migrated | Medium | Regression |
| Order Creation | Working | In progress | High | Full workflow |
| Push Notifications | Working | In progress | High | Device testing |
| Offline Sync | Working | Not started | Very High | Scenario testing |
This changes the migration from:
"The application is 60% migrated."
to:
"Authentication and customer workflows are migrated; offline synchronization and notifications remain."
That is much more useful to an engineering team.
3. Freeze Architecture, Not Development
A large enterprise application may need to continue receiving new features while migration is happening.
This creates a difficult situation.
The Xamarin application is still in production, but the team is building the future MAUI version.
Do not freeze all development for months.
Instead, establish a rule:
New feature
│
├── Business logic → Shared modern layer
│
├── Xamarin UI → Temporary implementation
│
└── MAUI UI → Preferred implementation
The important part is that new business logic should not become more dependent on Xamarin.
For example, avoid introducing:
public class OrderService
{
Xamarin.Forms.Application.Current...
}
Instead:
public class OrderService
{
private readonly IOrderRepository _repository;
public OrderService(IOrderRepository repository)
{
_repository = repository;
}
}
The service can then be consumed by both generations of the application.
This is one of the most effective ways to prevent the migration from becoming a moving target.
4. Use a Compatibility Layer Instead of Rewriting Everything
Large Xamarin applications often contain hundreds of dependencies on platform-specific functionality.
Trying to replace everything immediately creates unnecessary work.
Introduce interfaces around unstable or platform-specific functionality.
For example:
public interface IDeviceService
{
Task<string?> GetDeviceIdAsync();
Task<bool> IsNetworkAvailableAsync();
}
The implementation can change:
Xamarin
↓
XamarinDeviceService
MAUI
↓
MauiDeviceService
The application code does not need to know which implementation is being used.
This approach is particularly useful for:
- Device information
- Secure storage
- File access
- Connectivity
- Location
- Notifications
- Logging
- Analytics
- Authentication
- Background processing
The goal is not to create abstractions everywhere.
Create them specifically where migration or platform differences are expected.
5. Treat Custom Renderers as a Separate Migration Problem
Custom renderers are frequently one of the biggest sources of migration effort.
A typical Xamarin application may contain:
CustomRenderer
├── Android
├── iOS
├── Shared Control
└── Platform-specific behavior
In MAUI, handlers provide the preferred customization model.
However, you do not necessarily need to rewrite every renderer on day one. MAUI can reuse certain Xamarin.Forms renderer implementations through compatibility/shim support, although this approach has limitations and becomes more important to revisit as you move to newer MAUI versions.
A practical strategy is:
Existing Renderer
│
├── Simple + stable
│ ↓
│ Temporary reuse
│
├── Complex but required
│ ↓
│ Migrate later
│
└── Obsolete behavior
↓
Delete it
Do not automatically convert every renderer to a handler.
First ask:
Why does this renderer exist?
Sometimes the requirement disappeared years ago.
6. Be Aggressive About Removing Dead Xamarin Code
Migration is an excellent opportunity to identify code that no longer has a purpose.
Large Xamarin applications often contain:
- Old platform workarounds
- Deprecated controls
- Duplicate services
- Unused renderers
- Legacy navigation code
- Old dependency injection registrations
- Abandoned feature flags
- Xamarin-specific utilities
- Compatibility packages added years ago
Don't migrate these blindly.
Use this decision:
Does the code still provide business value?
│
├── Yes → Migrate
│
├── Maybe → Validate usage
│
└── No → Delete
Every line of legacy code that you remove is one less thing the MAUI application needs to maintain.
7. Migrate Dependencies Before UI
A common approach is:
Page 1
Page 2
Page 3
Page 4
...
This looks productive, but it can be misleading.
Suppose 80 pages depend on:
OldControls
OldDatabase
OldNavigation
OldAuthentication
Migrating the pages first means the same dependency problems appear repeatedly.
Instead, migrate foundational dependencies first:
API Client
↓
Authentication
↓
Storage
↓
Database
↓
Telemetry
↓
Navigation
↓
UI
Once the foundation is stable, migrating individual screens becomes significantly more predictable.
Microsoft's migration guidance similarly recommends updating dependencies and ensuring the Xamarin.Forms application is stable before moving further into the migration.
8. Don't Assume Every NuGet Package Has a MAUI Replacement
This is where many enterprise migrations get stuck.
You may discover:
Xamarin Package
↓
No MAUI version
Don't immediately search for a package with a similar name.
Classify the dependency:
Option 1 — Replace
There is a maintained MAUI/.NET equivalent.
Option 2 — Remove
The application no longer needs it.
Option 3 — Wrap
The package can remain behind an interface temporarily.
Option 4 — Fork
The package is important but abandoned and the organization controls the source.
Option 5 — Rewrite
The dependency is small enough to replace with internal code.
The worst strategy is:
"Let's keep searching until we find something that looks similar."
A package replacement can introduce completely different runtime behavior.
9. Separate UI Migration From Business Logic Migration
Suppose you have:
OrderPage
OrderViewModel
OrderService
OrderRepository
OrderDatabase
Do not migrate these as one block.
A better approach is:
OrderService → Modern .NET
OrderRepository → Modern .NET
OrderDatabase → Modern .NET
OrderViewModel → Modernized
OrderPage → MAUI
This allows the business layer to stabilize before the UI is converted.
The result should look approximately like:
┌───────────────┐
│ MAUI UI │
└───────┬───────┘
│
┌───────▼───────┐
│ ViewModel │
└───────┬───────┘
│
┌───────▼───────┐
│ Application │
│ Services │
└───────┬───────┘
│
┌───────▼───────┐
│ Infrastructure│
└───────────────┘
This is more valuable than simply changing namespaces.
10. Build One Complete Vertical Slice
Don't try to migrate 30 screens simultaneously.
Pick one representative business workflow.
For example:
Login
↓
Customer Search
↓
Customer Details
↓
Create Order
↓
Save Locally
↓
Submit
↓
Receive Notification
Migrate the complete workflow.
Why?
Because this exposes problems that a single migrated screen will not reveal:
- Navigation
- Authentication
- API calls
- Dependency injection
- Local storage
- Serialization
- Native APIs
- Error handling
- Logging
- Push notifications
- Performance
- App lifecycle
If the vertical slice works, you have a template for subsequent migrations.
11. Keep Xamarin and MAUI Builds Running in Parallel
For large applications, one of the safest approaches is to maintain:
Shared Core
│
┌────────┴────────┐
│ │
Xamarin App MAUI App
│ │
Production Migration
The shared layer contains business logic and infrastructure that can be reused.
The UI/platform layer gradually moves to MAUI.
This prevents the migration branch from becoming isolated for six months.
It also means developers can continue delivering fixes to the existing application.
12. Make the CI Pipeline a Migration Gate
A migration is incomplete if it only works on a developer's machine.
The CI pipeline should build:
Android
iOS
and eventually all supported targets.
At minimum, validate:
Restore
↓
Build
↓
Unit Tests
↓
UI/Integration Tests
↓
Signing
↓
Package
↓
Artifact Validation
Also validate the things developers frequently forget:
- App identifier
- Version number
- Signing certificates
- Provisioning profiles
- Android keystore
- Entitlements
- Push notification configuration
- Environment configuration
- API endpoints
- Release symbols
- Crash reporting
- Store packaging
A successful local build is not equivalent to a successful enterprise migration.
13. Test Native Features on Real Devices Early
One of the biggest mistakes is leaving native functionality until the end.
The application may compile perfectly while these features fail at runtime:
Push notifications
Background services
Location
Bluetooth
Camera
File picker
Deep links
Biometric authentication
Audio
Native SDKs
Create a native-risk prototype early.
For example:
MAUI Prototype
├── Push notification
├── Background task
├── Location
├── Camera
└── Third-party SDK
Run it on actual Android and iOS devices.
This gives you answers before hundreds of screens depend on those capabilities.
14. Pay Special Attention to Android and iOS Lifecycle Differences
Xamarin applications often contain lifecycle assumptions that are no longer safe to carry forward unchanged.
Examples include:
Application startup
Activity lifecycle
Background execution
Notification handling
Permission flow
Process termination
App resume
App sleep
Don't test only:
"Does the screen open?"
Test:
Open app
↓
Background app
↓
Kill app
↓
Receive notification
↓
Tap notification
↓
Resume app
↓
Network unavailable
↓
Network restored
Enterprise bugs frequently appear in these transitions rather than in normal UI navigation.
15. Offline Synchronization Deserves Its Own Migration Plan
Offline applications are particularly difficult to migrate.
Do not treat offline storage as simply:
"Move SQLite code to MAUI."
You need to validate:
Create offline record
↓
Modify offline record
↓
Queue operation
↓
Application terminated
↓
Network restored
↓
Synchronize
↓
Server conflict
↓
Retry
Compare Xamarin and MAUI behavior using the same test dataset.
For synchronization systems, correctness is much more important than simply getting the database to compile.
16. Use Feature Flags During the Transition
Feature flags can reduce migration risk.
For example:
if (FeatureFlags.UseMauiOrderFlow)
{
return await _mauiOrderFlow.ExecuteAsync();
}
return await _legacyOrderFlow.ExecuteAsync();
This allows a migration to be released gradually.
Possible rollout:
Internal users
↓
QA
↓
10% users
↓
25% users
↓
50% users
↓
100% users
The exact rollout depends on the organization's release process, but the principle is important:
Don't make the first production release the first real-world test.
17. Compare Production Metrics, Not Just Screens
A MAUI application can look identical to the Xamarin version and still behave differently.
Compare:
Startup time
Memory usage
Crash rate
ANR rate
API failures
Login success
Screen load time
Database operations
Push delivery
Background task success
Synchronization failures
For example:
Metric Xamarin MAUI
Startup 2.8 sec 3.1 sec
Crash-free sessions 99.4% 99.5%
Sync failures 0.8% 0.7%
Login failures 0.4% 0.5%
The exact numbers aren't important here.
The important thing is having measurable evidence instead of relying on:
"It seems to work."
18. Create a Migration Definition of Done
A screen should not be considered migrated merely because it compiles.
For each feature, define:
[ ] UI migrated
[ ] Navigation verified
[ ] API verified
[ ] Authentication verified
[ ] Offline behavior verified
[ ] Error handling verified
[ ] Analytics verified
[ ] Logging verified
[ ] Android tested
[ ] iOS tested
[ ] Regression tests passed
[ ] Production metrics available
For native features, add:
[ ] App backgrounded
[ ] App terminated
[ ] App restarted
[ ] Permission denied
[ ] Permission granted
[ ] Network unavailable
[ ] Network restored
This turns "migration complete" into something measurable.
19. What I Would Avoid
There are several approaches that look attractive but create problems later.
Big-bang migration
Stop Xamarin development
↓
Migrate everything
↓
Test everything
↓
Release
This creates a very large feedback loop.
Automatic conversion followed by mass bug fixing
Automation is useful, but it cannot understand business behavior.
Rewriting the entire application
Migration is not automatically a reason to rewrite business logic.
Migrating every renderer immediately
Some renderers can be temporarily reused or may no longer be required.
Replacing every NuGet package
A replacement package is not necessarily behaviorally equivalent.
Testing only the happy path
Enterprise applications fail in lifecycle, offline, permission, synchronization, and recovery scenarios.
20. A More Practical Migration Model
For a large enterprise application, I would structure the work like this:
EXISTING XAMARIN
│
▼
┌──────────────────┐
│ Dependency Audit │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Modern Core │
│ Services │
└────────┬─────────┘
│
┌────────┴────────┐
▼ ▼
Xamarin UI MAUI UI
│ │
│ Vertical Slice
│ │
└────────┬────────┘
▼
Feature Flags
│
▼
Canary Users
│
▼
Production MAUI
│
▼
Remove Xamarin Code
This is essentially a controlled strangler approach rather than a rewrite.
The old application continues to work while the new implementation progressively takes over.
Final Thoughts
The hardest part of Xamarin-to-MAUI migration isn't learning .NET MAUI.
Most experienced Xamarin developers can learn the MAUI differences relatively quickly.
The difficult part is managing the technical dependencies, production risk, and migration sequencing of a large application.
The most effective practical principles are:
- Map the application before changing it.
- Migrate dependencies before hundreds of screens.
- Keep business logic independent of Xamarin.
- Use compatibility layers where they reduce risk.
- Don't migrate obsolete code.
- Build one complete vertical slice first.
- Keep Xamarin and MAUI development running in parallel where necessary.
- Test native functionality on real devices early.
- Use feature flags and controlled rollout.
- Measure production behavior instead of judging migration by compilation.
.NET MAUI gives organizations a modern platform, but the migration itself should be treated as an engineering transformation rather than a project-file conversion.
The goal isn't:
"We converted Xamarin to MAUI."
The real goal is:
"We moved the production application to MAUI without losing business functionality, reliability, or the ability to keep delivering."
That distinction is what makes a large migration manageable.
By Niladri
Top comments (0)