Migrating from Xamarin.Forms to .NET MAUI: A Practical Guide
Xamarin.Forms served us well, but its time has passed. Microsoft ended support in May 2024, and .NET MAUI is the clear successor. If you're still running a Xamarin.Forms app in production, it's time to migrate. Here's how to do it without losing your mind.
What Actually Changed
MAUI isn't a rewrite — it's an evolution. The core concepts of XAML-based UI, data binding, and cross-platform abstraction remain. But the project structure, build system, and several APIs have changed significantly.
The biggest structural change is the move to a single-project architecture. Where Xamarin.Forms had separate platform projects (iOS, Android, UWP), MAUI consolidates everything into one project with platform-specific folders.
Step 1: Assess Your Project
Before touching any code, audit your dependencies. Run through your NuGet packages and check which ones have MAUI-compatible versions. Most popular libraries — Prism, ReactiveUI, SkiaSharp — have MAUI builds. If a critical dependency hasn't been ported, you'll need a replacement.
<!-- Old Xamarin.Forms package reference -->
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2578" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.7" />
<!-- New MAUI equivalents — Essentials is now built in -->
<PackageReference Include="Microsoft.Maui.Controls" Version="9.0.0" />
Note that Xamarin.Essentials is now baked into MAUI. You no longer need a separate package for device APIs like geolocation, preferences, or connectivity.
Step 2: Create the New Project Structure
The easiest migration path is to create a fresh MAUI project and move your code across, rather than trying to convert the .csproj in place.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net9.0-android;net9.0-ios;net9.0-maccatalyst</TargetFrameworks>
<OutputType>Exe</OutputType>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
</PropertyGroup>
</Project>
Your platform-specific code now lives under Platforms/Android, Platforms/iOS, and so on within the same project.
Step 3: Update Namespaces
This is the most tedious part. Every Xamarin.Forms namespace becomes Microsoft.Maui:
// Before
using Xamarin.Forms;
using Xamarin.Essentials;
// After
using Microsoft.Maui;
using Microsoft.Maui.Controls;
A simple find-and-replace across your solution handles most cases. The XAML namespace also changes:
<!-- Before -->
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms">
<!-- After -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui">
Step 4: Replace the Application Bootstrap
Xamarin.Forms used App.xaml.cs with a MainPage assignment. MAUI uses a builder pattern with MauiProgram.cs:
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
builder.Services.AddSingleton<MainViewModel>();
builder.Services.AddTransient<MainPage>();
return builder.Build();
}
}
This is a significant improvement — you now get proper dependency injection out of the box, using the same Microsoft.Extensions.DependencyInjection container that ASP.NET Core uses.
Step 5: Handle Breaking API Changes
Several APIs have been renamed or restructured:
Device.BeginInvokeOnMainThreadbecomesMainThread.BeginInvokeOnMainThreadDevice.RuntimePlatformis replaced by conditional compilation orDeviceInfo.Platform- Custom renderers are replaced by handlers (a far cleaner abstraction)
Init()calls in platform projects are no longer needed
// Before — checking platform at runtime
if (Device.RuntimePlatform == Device.iOS)
{
Padding = new Thickness(0, 20, 0, 0);
}
// After — using DeviceInfo
if (DeviceInfo.Platform == DevicePlatform.iOS)
{
Padding = new Thickness(0, 20, 0, 0);
}
Common Pitfalls
NuGet package conflicts. Some packages still pull in transitive Xamarin dependencies. Check your dependency tree carefully with dotnet list package --include-transitive.
Custom renderers. If you have custom renderers, they won't compile. You'll need to rewrite them as handlers. The handler architecture is more efficient, but the migration is manual work.
Effects. MAUI still supports effects, but the registration mechanism has changed. Consider migrating them to platform behaviours or handlers instead.
Android manifest and iOS Info.plist. These still exist but live in different locations. Double-check your permissions and capabilities have carried across.
Is It Worth It?
Absolutely. Beyond the obvious benefit of continued support and security patches, MAUI brings genuine performance improvements — particularly on Android where startup times are noticeably better. The handler architecture is cleaner than renderers, the DI integration simplifies your code, and single-project management reduces the overhead of maintaining platform-specific projects.
Start with a small, low-risk app if you have multiple Xamarin projects. Get the migration process nailed down, then tackle the larger ones.