Migrating a Large Enterprise Xamarin Application to .NET MAUI: Practical Strategies That Actually Work
1. Don't Start With the .csproj
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.
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:
- Replace - There is a maintained MAUI/.NET equivalent.
- Remove - The application no longer needs it.
- Wrap - The package can remain behind an interface temporarily.
- Fork - The package is important but abandoned and the organization controls the source.
- 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 AppMAUI App
│ │ ProductionMigration
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:
MetricXamarin MAUI Startup 2.8 sec 3.1 sec Crash-free sessions 99.4% 99.5% Sync failures 0.8%0.7% Login failures0.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 attract
Comments
No comments yet. Start the discussion.