TL;DR

Thay vì nhồi #if ANDROID / #if IOS khắp một service, bạn có thể tách code platform-specific ra file riêng theo suffix tên file (MyService.Android.cs, MyService.iOS.cs) và để chúng cùng là partial class với file shared MyService.cs. Build system của .NET MAUI multi-target sẽ chỉ compile đúng file cho TFM đang build. Kết quả: zero conditional compilation, IntelliSense đầy đủ cho từng platform, file ngắn, refactor an toàn.

Vì sao #if trở thành rác

Cách phổ biến nhất khi cần gọi platform API trong shared code là conditional compilation. Microsoft Learn đưa ngay ví dụ này:

#if ANDROID
    IWindowManager windowManager = Android.App.Application.Context.GetSystemService(Context.WindowService).JavaCast<IWindowManager>();
    SurfaceOrientation orientation = windowManager.DefaultDisplay.Rotation;
    bool isLandscape = orientation == SurfaceOrientation.Rotation90 || orientation == SurfaceOrientation.Rotation270;
    return isLandscape ? DeviceOrientation.Landscape : DeviceOrientation.Portrait;
#elif IOS
    UIInterfaceOrientation orientation = UIApplication.SharedApplication.StatusBarOrientation;
    bool isPortrait = orientation == UIInterfaceOrientation.Portrait || orientation == UIInterfaceOrientation.PortraitUpsideDown;
    return isPortrait ? DeviceOrientation.Portrait : DeviceOrientation.Landscape;
#else
    return DeviceOrientation.Undefined;
#endif

Một method ngắn còn ổn. Nhưng khi service phình lên 5–10 method, mỗi method 3 nhánh platform, file biến thành mê cung. IntelliSense chỉ active đúng 1 nhánh tại một thời điểm — sửa nhánh Android xong push đi, nhánh iOS có thể đã sai cú pháp mà bạn không hề biết cho tới khi CI build iOS.

Partial class: cùng tên, cùng namespace, khác file

Pattern thay thế dựa trên tính năng partial class + partial method của C#. Bạn khai báo signature ở file shared, implement ở file platform — và compiler ghép chúng lại như cùng một class.

File shared (Services/DeviceOrientationService.cs, để ngoài thư mục Platforms/):

namespace InvokePlatformCodeDemos.Services.PartialMethods;

public partial class DeviceOrientationService
{
    public partial DeviceOrientation GetOrientation();
}

File Android (Services/DeviceOrientationService.Android.cs):

using Android.Content;
using Android.Runtime;
using Android.Views;

namespace InvokePlatformCodeDemos.Services.PartialMethods;

public partial class DeviceOrientationService
{
    public partial DeviceOrientation GetOrientation()
    {
        IWindowManager windowManager = Android.App.Application.Context.GetSystemService(Context.WindowService).JavaCast<IWindowManager>();
        SurfaceOrientation orientation = windowManager.DefaultDisplay.Rotation;
        bool isLandscape = orientation == SurfaceOrientation.Rotation90 || orientation == SurfaceOrientation.Rotation270;
        return isLandscape ? DeviceOrientation.Landscape : DeviceOrientation.Portrait;
    }
}

File iOS (Services/DeviceOrientationService.iOS.cs):

using UIKit;

namespace InvokePlatformCodeDemos.Services.PartialMethods;

public partial class DeviceOrientationService
{
    public partial DeviceOrientation GetOrientation()
    {
        UIInterfaceOrientation orientation = UIApplication.SharedApplication.StatusBarOrientation;
        bool isPortrait = orientation == UIInterfaceOrientation.Portrait || orientation == UIInterfaceOrientation.PortraitUpsideDown;
        return isPortrait ? DeviceOrientation.Portrait : DeviceOrientation.Landscape;
    }
}

Phía consumer chỉ thấy new DeviceOrientationService().GetOrientation() — không cần biết platform. Mỗi file platform có IntelliSense riêng cho native SDK của nó (UIKit, Android.Views) — full type-checking, không có vùng code "mờ" như #if.

Hai cách multi-target: folder vs filename

.NET MAUI cho bạn chọn 1 trong 2 (hoặc kết hợp):

1. Folder-based (mặc định, không cần config)

Đặt file vào đúng Platforms/Android/, Platforms/iOS/, ... Build system tự loại trừ folder không match TFM. Hợp với app — code platform thường ít, gom 1 chỗ tiện.

2. Filename-based (suffix .Platform.cs — cần config csproj)

File nằm cạnh code shared, đặt tên kết thúc bằng .Android.cs, .iOS.cs... — discoverability cao, code platform nằm sát logic chung. Đây là cách Gerald Versluis (MAUI PM) khuyến nghị cho service lớn và library tác giả.

Pattern này không được wire-up sẵn — bạn phải thêm vào .csproj:

<!-- Android -->
<ItemGroup Condition="$(TargetFramework.StartsWith('net8.0-android')) != true">
  <Compile Remove="**\*.Android.cs" />
  <None Include="**\*.Android.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>

<!-- iOS -->
<ItemGroup Condition="$(TargetFramework.StartsWith('net8.0-ios')) != true">
  <Compile Remove="**\*.iOS.cs" />
  <None Include="**\*.iOS.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>

<!-- MacCatalyst -->
<ItemGroup Condition="$(TargetFramework.StartsWith('net8.0-maccatalyst')) != true">
  <Compile Remove="**\*.MacCatalyst.cs" />
  <None Include="**\*.MacCatalyst.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>

<!-- Windows -->
<ItemGroup Condition="$(TargetFramework.Contains('-windows')) != true">
  <Compile Remove="**\*.Windows.cs" />
  <None Include="**\*.Windows.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>

Có thêm .MaciOS.cs nếu muốn 1 file dùng chung cho cả iOS và MacCatalyst. Bảng đầy đủ:

SuffixCompile khi TFM
.Android.csbắt đầu bằng net8.0-android
.iOS.csbắt đầu bằng net8.0-ios
.MacCatalyst.csbắt đầu bằng net8.0-maccatalyst
.MaciOS.csiOS hoặc MacCatalyst
.Windows.cschứa -windows

Khi nào dùng cái nào

  • Vẫn dùng #if cho one-liner thực sự ngắn (1–2 dòng khác biệt), hoặc khi cần fallback graceful (return Undefined trên platform chưa support).
  • Dùng partial class + folder cho app size vừa, code platform tập trung dưới Platforms/.
  • Dùng partial class + filename suffix cho service lớn, library NuGet, hoặc khi muốn code platform "đứng cạnh" logic shared để dễ navigate.

Lưu ý quan trọng

  • Mọi part phải cùng namespace, cùng class name. Sai 1 ký tự — không ghép được, build fail.
  • Bạn phải implement partial method trên mọi TFM khai báo trong <TargetFrameworks>. Quên 1 platform = build platform đó fail. Đây là trade-off so với #if: mất khả năng "skip nhẹ nhàng".
  • partial method có return value bắt buộc phải có implementation (C# spec).
  • Khi thêm TFM mới (ví dụ Tizen) — đừng quên cập nhật ItemGroup trong .csproj nếu xài filename suffix.
  • Zero runtime cost — toàn bộ multi-targeting xử lý ở compile-time.

Kết

Với một service nhỏ, #if tạm ổn. Nhưng ngay khi class chạm 100+ dòng hoặc bạn ship library cho người khác xài, partial class + filename suffix là chuẩn industry mà MAUI team khuyến nghị. Code đọc dễ hơn, IDE thông minh hơn, regression giảm hẳn.

Nguồn: Microsoft Learn — Invoking platform code, Configure MAUI multi-targeting, tip gốc của Gerald Versluis.