Xamarin to .NET MAUI Migration: A Practical Guide with AI-Assisted Code Translation
Xamarin.Forms to .NET MAUI is the most direct mobile migration path available: same language, similar patterns, and Microsoft provides migration tooling. But “most direct” does not mean trivial. Custom Renderers to Handlers, project restructuring, and DI changes need careful handling.
- The single-project structure replaces the old multi-project Xamarin.Forms solution layout.
- Custom Renderers to Handlers is the most significant breaking change and the most time-consuming migration task.
- XAML namespace changes are mechanical and handled well by AI tooling.
- DependencyService is replaced by built-in .NET dependency injection.
- AI-assisted tooling handles 55-70% of the mechanical migration work. The rest requires human review, particularly for visual QA on devices.
Your Xamarin.Forms app works. It just cannot stay where it is.
Xamarin reached end of life in May 2024. Your Xamarin.Forms app still runs, still passes tests, and still ships to the App Store, for now. But the framework underneath it has stopped moving. Apple’s SDK requirements advance annually. Your framework does not.
.NET MAUI is the intended successor. It shares C# and XAML with Xamarin.Forms. It runs on .NET 10. It targets the latest iOS and Android SDKs. For most Xamarin.Forms applications, it is the lowest-friction migration path available.
This guide walks through the migration step by step.
Before you start: is MAUI the right target?
MAUI is the right target for most Xamarin.Forms apps. But not all. Consider React Native instead if:
- Your app is Xamarin.Native (Xamarin.iOS / Xamarin.Android without Xamarin.Forms)
- You want a broader developer ecosystem and hiring market
- The app needs a significant UI redesign alongside the migration
- Your team has JavaScript/TypeScript expertise
For the comparison, see our MAUI vs React Native analysis. For PhoneGap/Cordova apps, MAUI is not an option. See our PhoneGap to React Native guide.
Phase 1: Project restructuring
From multi-project to single-project
Xamarin.Forms solutions typically contain:
MyApp.sln
├── MyApp/ (shared .NET Standard library)
├── MyApp.iOS/ (iOS platform project)
├── MyApp.Android/ (Android platform project)
└── MyApp.UWP/ (optional UWP project)
MAUI uses a single project with platform-specific folders:
MyApp.sln
└── MyApp/
├── Platforms/
│ ├── iOS/
│ ├── Android/
│ ├── MacCatalyst/
│ └── Windows/
├── Resources/
├── MauiProgram.cs
└── MyApp.csproj
How to migrate the project structure:
- Create a new MAUI project using
dotnet new maui -n MyApp - Copy shared code (ViewModels, Services, Models) into the new project
- Move platform-specific code into the appropriate
Platforms/subfolder - Migrate resources (images, fonts) into the
Resources/folder using MAUI’s resource system
The .NET Upgrade Assistant can perform this restructuring automatically:
dotnet tool install -g upgrade-assistant
upgrade-assistant upgrade MyApp.sln --target-tfm net10.0
Review the output carefully. The Upgrade Assistant handles the project file conversion well but can miss custom build configurations and platform-specific MSBuild properties.
NuGet package updates
Xamarin-specific packages need replacing:
| Xamarin Package | MAUI Equivalent |
|---|---|
Xamarin.Forms | Microsoft.Maui.Controls (included in MAUI) |
Xamarin.Essentials | Microsoft.Maui.Essentials (included in MAUI) |
Xamarin.CommunityToolkit | CommunityToolkit.Maui |
Xamarin.CommunityToolkit.Markup | CommunityToolkit.Maui.Markup |
Claude Code handles these package replacements when analysing the .csproj files.
Phase 2: XAML migration
Namespace changes
Every XAML file needs its root namespace updated:
<!-- BEFORE -->
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MyApp.Views">
<!-- AFTER -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MyApp.Views">
This is a mechanical find-and-replace across every XAML file. Claude Code handles it with high accuracy. For a 50-screen app, this takes seconds with AI tooling and hours manually.
Control API changes
Most Xamarin.Forms controls exist in MAUI with the same name. Some have API changes:
<!-- BEFORE: Xamarin.Forms Frame -->
<Frame HasShadow="True" CornerRadius="10" Padding="10">
<Label Text="Content" />
</Frame>
<!-- AFTER: MAUI Border (Frame is available but Border is preferred) -->
<Border StrokeShape="RoundRectangle 10"
Padding="10"
Shadow="{Shadow Brush=Black, Offset='2,2', Opacity=0.3}">
<Label Text="Content" />
</Border>
<!-- BEFORE: Xamarin.Forms SearchBar -->
<SearchBar Placeholder="Search"
SearchButtonPressed="OnSearch" />
<!-- AFTER: MAUI SearchBar (same, but event handling may use Command) -->
<SearchBar Placeholder="Search"
SearchCommand="{Binding SearchCommand}" />
Claude Code identifies these API differences and applies the correct MAUI equivalents. The CLAUDE.md file should specify whether to use the MAUI-preferred approach (Border over Frame, Commands over events) or maintain maximum compatibility.
Phase 3: Custom Renderers to Handlers
This is the most significant migration task. Xamarin.Forms Custom Renderers gave you full control over how a control rendered on each platform, but the API was complex and tightly coupled to the Xamarin rendering pipeline. MAUI Handlers are simpler and more maintainable, but the architecture is fundamentally different.
Understanding the difference
Custom Renderer pattern (Xamarin):
- Inherits from a platform-specific renderer base class
- Overrides
OnElementChangedandOnElementPropertyChanged - Accesses the native control via
Controlproperty - Registered via assembly attribute
Handler pattern (MAUI):
- Uses a property mapper to map cross-platform properties to platform-specific implementations
- Platform-specific code lives in partial classes
- Accesses the native control via
PlatformViewproperty - Registered in
MauiProgram.cs
Migration example: a custom entry with rounded corners
// BEFORE: Xamarin Custom Renderer (iOS)
[assembly: ExportRenderer(typeof(RoundedEntry),
typeof(RoundedEntryRenderer))]
namespace MyApp.iOS.Renderers
{
public class RoundedEntryRenderer : EntryRenderer
{
protected override void OnElementChanged(
ElementChangedEventArgs<Entry> e)
{
base.OnElementChanged(e);
if (Control == null) return;
Control.Layer.CornerRadius = 10;
Control.Layer.BorderWidth = 1;
Control.Layer.BorderColor = UIColor.SystemGray.CGColor;
Control.ClipsToBounds = true;
}
}
}
// AFTER: MAUI Handler (cross-platform definition)
public partial class RoundedEntryHandler : EntryHandler
{
public RoundedEntryHandler() : base(PropertyMapper) { }
public static new IPropertyMapper<IEntry, RoundedEntryHandler>
PropertyMapper =
new PropertyMapper<IEntry, RoundedEntryHandler>(
EntryHandler.Mapper)
{
[nameof(IView.Background)] = MapBackground
};
}
// iOS partial class (in Platforms/iOS/)
public partial class RoundedEntryHandler
{
private static void MapBackground(
RoundedEntryHandler handler, IEntry entry)
{
handler.PlatformView.Layer.CornerRadius = 10;
handler.PlatformView.Layer.BorderWidth = 1;
handler.PlatformView.Layer.BorderColor =
UIColor.SystemGray.CGColor;
handler.PlatformView.ClipsToBounds = true;
}
}
// Registration in MauiProgram.cs
builder.ConfigureMauiHandlers(handlers =>
{
handlers.AddHandler<RoundedEntry, RoundedEntryHandler>();
});
Claude Code handles this structural conversion well because the mapping is well-defined. The key areas where human review is needed:
- Renderers that access
Elementproperties beyond standard control properties - Renderers with complex lifecycle management (creating/disposing native views)
- Renderers that use platform-specific gestures or animations
How many renderers do you have?
The number of Custom Renderers is the single best predictor of Xamarin to MAUI migration complexity. Count them before estimating:
| Custom Renderers | Migration complexity | Typical timeline impact |
|---|---|---|
| 0-5 | Low | 1-2 weeks for renderer migration |
| 6-15 | Moderate | 2-4 weeks for renderer migration |
| 16-30 | High | 4-6 weeks for renderer migration |
| 30+ | Very high | 6+ weeks; consider whether MAUI is still the right target |
Phase 4: Dependency injection migration
Xamarin.Forms used DependencyService for platform-specific service resolution. MAUI uses the built-in .NET dependency injection container.
// BEFORE: Xamarin DependencyService registration (attribute-based)
[assembly: Dependency(typeof(LocationService))]
namespace MyApp.iOS.Services
{
public class LocationService : ILocationService
{
public async Task<Location> GetCurrentLocation()
{
// iOS-specific implementation
}
}
}
// Usage in shared code
var location = DependencyService.Get<ILocationService>();
// AFTER: MAUI built-in DI
// Registration in MauiProgram.cs
#if IOS
builder.Services.AddSingleton<ILocationService,
Platforms.iOS.LocationService>();
#elif ANDROID
builder.Services.AddSingleton<ILocationService,
Platforms.Android.LocationService>();
#endif
// Usage via constructor injection
public class MapViewModel(ILocationService locationService)
{
public async Task UpdateLocation()
{
var location = await locationService.GetCurrentLocation();
}
}
Claude Code migrates DependencyService registrations to MauiProgram.cs registrations systematically. The human decision: whether to use platform-specific compile directives (#if IOS) or MAUI’s conditional service registration. For most apps, compile directives are clearer.
Phase 5: Navigation migration
Xamarin.Forms offered multiple navigation patterns: NavigationPage, TabbedPage, FlyoutPage, and manual PushAsync/PopAsync calls. MAUI consolidates these under Shell navigation.
// BEFORE: Xamarin NavigationPage
await Navigation.PushAsync(new OrderDetailPage(orderId));
// AFTER: MAUI Shell navigation
await Shell.Current.GoToAsync(
$"orderDetail?orderId={orderId}");
Shell navigation is route-based, which is cleaner for deep linking and URL-based navigation. The migration involves:
- Defining routes in
AppShell.xaml - Registering route parameters with
[QueryProperty]attributes - Replacing
PushAsync/PopAsynccalls withGoToAsync
This is a systematic change that Claude Code handles well, though human review is important for navigation flows that depend on the stack order (e.g., clearing the navigation stack, modal presentations).
Phase 6: Testing and device verification
After all code changes are complete, testing happens in three layers:
Unit tests. If you had unit tests for ViewModels and services, they should pass with minimal changes. Test framework migration (if needed) follows the same patterns as backend .NET migration.
Integration tests. API clients, data access, and service interactions should be tested against real (or stubbed) backends.
Device testing. This cannot be automated by AI. Every screen must be tested on physical iOS and Android devices. Focus on:
- Visual parity with the Xamarin version (padding, font rendering, control sizing)
- Touch targets and gesture recognition
- Orientation changes
- Accessibility (VoiceOver on iOS, TalkBack on Android)
- Performance on lower-end devices (MAUI’s Android startup time is slower than Xamarin in some configurations)
What we have seen in practice
[CLIENT EXAMPLE: Insurance company, Xamarin.Forms claims app, 52 screens, 18 Custom Renderers, Syncfusion data grid and chart components. Migrated to MAUI over 6 weeks. Custom Renderer migration was the critical path (3 weeks). XAML namespace changes and DependencyService migration were handled almost entirely by Claude Code (~65% AI-authored). The Syncfusion migration was smoother than expected: most API names matched. Key issue: two Custom Renderers that used MessagingCenter for cross-component communication needed manual redesign to use WeakReferenceMessenger.]
[CLIENT EXAMPLE: Healthcare provider, Xamarin.Forms patient check-in app, 28 screens, 8 Custom Renderers, camera integration for ID scanning. Migrated to MAUI over 4 weeks. The camera Custom Renderer was the most complex migration (used AVFoundation directly on iOS). AI-assisted tooling handled the standard renderers well. Total AI-authored code: ~60%. Key lesson: MAUI’s camera API has improved since Xamarin, so the custom renderer was simplified rather than directly translated.]
[CLIENT EXAMPLE: Logistics company, Xamarin.Forms driver app, 35 screens, 12 Custom Renderers, offline SQLite sync, Bluetooth LE for barcode scanners. Migrated to MAUI over 7 weeks. The Bluetooth LE custom renderer required platform-specific rewriting because the MAUI Bluetooth landscape had changed. SQLite migration was straightforward (sqlite-net-pcl works on both platforms). AI-assisted tooling was most effective on the XAML migration and ViewModel restructuring.]
Next steps
Before starting a Xamarin to MAUI migration, assess your application’s complexity using our approach from the mobile migration playbook. Count your Custom Renderers, catalogue your third-party components, and verify that MAUI equivalents exist.
For the decision between MAUI and React Native, see our honest comparison.
For IP considerations when using AI tools in migration, see Who Owns AI-Written Code?.
For the general modernisation decision framework, see Modernise, Rebuild, or Replace.
Ready to plan your migration? Book a free Mobile Migration Assessment consultation.
Frequently asked questions
Can I migrate Xamarin.Native apps to MAUI?
Do I need to migrate all Custom Renderers at once?
What happens to my Syncfusion or Telerik controls?
Will my MAUI app look and behave identically to the Xamarin.Forms version?
How long does a typical Xamarin.Forms to MAUI migration take?
Related guides
Modernise, Rebuild, or Replace: A Decision Framework for Legacy Systems
Six modernisation strategies explained in plain language. Decision criteria, cost and risk comparisons, and how AI-augmented delivery changes which options are viable.
Signs Your Legacy System Is Costing You More Than You Think
Legacy systems hide their true costs in maintenance burden, talent risk, security exposure, and missed opportunities. Eight warning signs and how AI-augmented analysis reveals the full picture.