Deep Linking ב-.NET MAUI 10: מדריך מלא ל-Universal Links, App Links ו-Shell Routes (2026)
כל מה שצריך כדי להטמיע Deep Linking ב-.NET MAUI 10: קבצי AASA ו-assetlinks.json, הגדרת intent-filter ו-Entitlements, קליטה ב-AppDelegate וב-MainActivity, וניתוב Shell חכם עם פרמטרים.
Deep Linking ב-.NET MAUI 10 הוא המנגנון שמאפשר לפתוח מסך ספציפי באפליקציה ישירות מתוך URL, התראה או אפליקציה אחרת, באמצעות שילוב של Shell URI routing פנימי, Universal Links של Apple ב-iOS, ו-App Links של Google ב-Android. במאמר הזה אני עובר על הצנרת המלאה: קובץ ה-apple-app-site-association שאפל דורשת, קובץ ה-assetlinks.json של Google, קונפיגורציית ה-intent-filter ב-AndroidManifest.xml, וההבדל בין OpenUrl ל-ContinueUserActivity ב-AppDelegate. אין כאן קסמים. יש כללים ברורים של הפלטפורמות, ואם מפספסים אחד מהם, הקישור פשוט לא ייפתח.
אני מעביר את הפרויקטים שלי מ-Xamarin.Forms כבר כמה חודשים, וההגדרה של deep links היא אחד מהמקומות הראשונים שבהם אתה מגלה עד כמה MAUI שונה מהאחיין הישן. אז בואו נסדר את זה פעם אחת ולתמיד.
ב-.NET MAUI 10 יש שלוש שכבות של deep linking: Shell URI routing (פנימי), Universal Links (iOS), ו-App Links (Android). לפתרון מקצה-לקצה צריך את שלושתן.
Universal Links דורשים קובץ apple-app-site-association ב-https://your-domain.com/.well-known/ ללא סיומת קובץ, מוגש כ-application/json, ו-Team ID תואם ב-Entitlements.plist.
App Links ב-Android דורשים assetlinks.json ב-/.well-known/, android:autoVerify="true" ב-intent-filter, וחתימה עם ה-SHA-256 fingerprint הרשמי של האפליקציה (Play App Signing).
יש לרשום routes באמצעות Routing.RegisterRoute ולנווט עם Shell.Current.GoToAsync תוך העברת פרמטרים כ-query string.
בדיקה: ב-iOS Simulator דרך xcrun simctl openurl; ב-Android Emulator דרך adb shell am start -W -a android.intent.action.VIEW -d.
הפרטים הקטנים הורגים: MIME type שגוי לקובץ AASA, autoVerify חסר, או fingerprint של debug במקום release. כל אחד מהם שובר את הקישור בשקט, בלי הודעת שגיאה.
מה זה Deep Linking ומה ההבדל בין הסוגים?
Deep Linking הוא היכולת לנווט ישירות למסך פנימי באפליקציית מובייל דרך URL, במקום להשאיר את המשתמש בדף הבית. באקוסיסטם של .NET MAUI, המונח מתפצל לשלוש טכנולוגיות שונות שעובדות יחד, אבל כל אחת פועלת בשכבה אחרת של המערכת:
Custom URI Schemes. הפורמט הישן (כמו myapp://product/42). זמין בכל פלטפורמה, פשוט להגדרה, אבל לא בטוח: כל אפליקציה יכולה לרשום את אותה סכימה. Apple ו-Google כבר לא ממליצות עליו לשימושי פרודקשן.
Universal Links (iOS). הסטנדרט של Apple מאז iOS 9. משתמש ב-URL רגיל של HTTPS (כמו https://shop.example.com/product/42), עם קובץ אימות בשם apple-app-site-association שיושב על הדומיין שלך. אם האפליקציה מותקנת ומאומתת, המערכת פותחת אותה; אחרת נפתח הדפדפן.
App Links (Android). הסטנדרט המקביל של Google מאז Android 6.0 (API 23). גם הוא משתמש ב-URL רגיל של HTTPS ובקובץ אימות (assetlinks.json). ההבדל המהותי: Android דורש חתימה קריפטוגרפית של האפליקציה שתואמת ל-SHA-256 fingerprint שרשום בקובץ, מה שהופך אותו לאבטחתי יותר מ-Universal Links.
ב-Shell של MAUI יש גם רמה רביעית: Shell URIs פנימיים (למשל //main/products?id=42) שמשמשים לניווט בתוך האפליקציה. שלושת ה-mechanisms החיצוניים מזינים בסוף לתוך אותה מכונת ניווט של Shell.
Shell URI Routing ב-.NET MAUI 10
לפני שנוגעים בקבצי אימות של פלטפורמות, צריך שהאפליקציה עצמה תדע לתרגם URI לפתיחת מסך. ב-.NET MAUI Shell זו העבודה של Routing.RegisterRoute. מבנה ה-URI הפנימי הוא //route/sub-route?param=value, ו-Shell מפרק אותו לסטאק ניווט אוטומטית.
נגדיר קודם route בסיסי. ב-AppShell.xaml.cs:
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
// Register routes for deep-linkable pages
Routing.RegisterRoute("product", typeof(ProductDetailPage));
Routing.RegisterRoute("order", typeof(OrderDetailPage));
Routing.RegisterRoute("settings/notifications", typeof(NotificationSettingsPage));
}
}
עכשיו אפשר לנווט מכל מקום באפליקציה עם:
await Shell.Current.GoToAsync($"product?id={productId}");
// Or with a full absolute route:
await Shell.Current.GoToAsync($"//main/product?id={productId}");
ה-Shell URIs נראים דומים ל-URLs אבל הם לא זהים. הם פועלים ברמת ה-view stack הפנימי. תפקידנו הוא לחבר URL חיצוני (HTTPS או custom scheme) ל-Shell URI המתאים. את זה נעשה בשלב הקליטה (ראה קטע קליטה למטה). התיעוד הרשמי של Microsoft ב-Shell Navigation מתאר את המערכת בפירוט, כולל הבדלים בין absolute ל-relative routes.
שים לב שכאשר האפליקציה משתמשת ב-Shell tabs (<TabBar>), אפשר לנווט ישירות לטאב ספציפי דרך ShellContent.Route. נגדיר ב-XAML Route="main" ואז GoToAsync("//main") תעבור לטאב הזה בלי לפתוח דף חדש בסטאק. זה חשוב כדי להימנע ממצב שבו דף מוצר מופיע מחוץ להקשר של ה-tab bar (טעות שעולה בפאנל בדיקות QA כמעט בכל release ראשון, מנסיוני).
iOS: הגדרת Universal Links שלב אחר שלב
ל-iOS יש שלושה רכיבים שחייבים לעבוד ביחד: קובץ AASA על הדומיין, capability של Associated Domains ב-Xcode/Entitlements, ו-handler בקוד. אם אחד מהם חסר או שגוי, הקישור פשוט ייפתח בדפדפן, בלי שגיאה גלויה. זו אחת המלכודות הכואבות ביותר בפיתוח iOS. הבזתי על זה יום שלם פעם, לפני שהבנתי שה-CDN שלי מחזיר text/plain במקום application/json.
שלב 1: קובץ apple-app-site-association על השרת
הקובץ חייב להיות זמין ב-https://your-domain.com/.well-known/apple-app-site-association, ללא סיומת .json, ומוגש עם Content-Type: application/json. אלה שלוש דרישות נפרדות ואף אחת מהן לא ניתנת לפשרה:
ה-appIDs בפורמט TEAMID.BundleId. את ה-Team ID אפשר לראות ב-App Store Connect או ב-Apple Developer portal. Apple טוענת את הקובץ הזה פעם אחת בהתקנת האפליקציה, לא כל פעם שנפתח קישור. לכן שינויים בו לא נכנסים לתוקף עד שהאפליקציה מותקנת מחדש (או שהמערכת מרעננת אותו, מה שקורה לפי לוח זמנים לא-פומבי).
שלב 2: Entitlements.plist ב-MAUI
ב-.NET MAUI, קובץ ה-Entitlements נמצא ב-Platforms/iOS/Entitlements.plist. יש להוסיף:
וב-.csproj של הפרויקט צריך לוודא ש-<CodesignEntitlements>Platforms/iOS/Entitlements.plist</CodesignEntitlements> מוגדר עבור configuration של Release. במידה ואתה מפרסם דרך Provisioning Profile ידני, ה-profile חייב לכלול את ה-capability של Associated Domains. אחרת ה-signing יכשל בשלב הארכיוב, ואת השגיאה של Xcode צריך לפרש בזהירות כי היא לא מציינת את הסיבה האמיתית.
שלב 3: קליטה ב-AppDelegate
קישורי Universal Links נכנסים דרך ContinueUserActivity ב-AppDelegate, לא דרך OpenUrl. זהו הבדל קריטי מ-custom URI schemes:
using Foundation;
using UIKit;
namespace ShopMaui;
[Register(nameof(AppDelegate))]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
public override bool ContinueUserActivity(UIApplication application,
NSUserActivity userActivity, UIApplicationRestorationHandler completionHandler)
{
if (userActivity.ActivityType == "NSUserActivityTypeBrowsingWeb"
&& userActivity.WebPageUrl is not null)
{
var url = userActivity.WebPageUrl.AbsoluteString;
_ = DeepLinkRouter.HandleAsync(url);
return true;
}
return base.ContinueUserActivity(application, userActivity, completionHandler);
}
}
Android: הגדרת App Links שלב אחר שלב
ב-Android העבודה מפורקת בין AndroidManifest.xml, קובץ assetlinks.json, ו-MainActivity. בניגוד ל-iOS, כאן המערכת מאמתת את הקישור פעם אחת בהתקנה ואז זוכרת את התוצאה. אם האימות נכשל, זה נשאר כשל עד ההתקנה הבאה של האפליקציה או עד שמריצים adb shell pm verify-app-links --re-verify.
שלב 1: intent-filter ב-AndroidManifest
ב-.NET MAUI 10 יש שתי דרכים להגדיר intent-filter: דרך attributes של MainActivity או דרך עריכה ידנית של Platforms/Android/AndroidManifest.xml. אני ממליץ על המסלול הידני כי הוא מפורש יותר, וקל יותר ל-code review לתפוס טעויות בו:
android:autoVerify="true" הוא ההבדל בין App Link ל-URI רגיל: הוא מבקש מהמערכת לאמת את הבעלות דרך assetlinks.json. בלעדיו, המשתמש יראה dialog של "פתח עם", מה שנחשב לחוויה גרועה ולא מקצועית. launchMode="singleTask" מונע יצירת instance כפול של האפליקציה בעת פתיחה מקישור.
שלב 2: קובץ assetlinks.json
הקובץ חי ב-https://your-domain.com/.well-known/assetlinks.json עם Content-Type: application/json. הוא מכיל את ה-SHA-256 fingerprint של תעודת החתימה של האפליקציה:
מקור ה-fingerprint תלוי בדרך הפרסום: אם אתה משתמש ב-Play App Signing (מומלץ, ומחייב לאפליקציות חדשות מאז 2021), את ה-SHA-256 תמצא ב-Play Console תחת Setup → App integrity → App signing key certificate. אם אתה חותם ידנית, השתמש ב-keytool -list -v -keystore your.keystore. חשוב מאוד: לפני שאתה מעלה ל-Play, השתמש ב-fingerprint של ה-upload key; אחרי שההעלאה עוברת ל-Play App Signing, הוסף גם את ה-fingerprint של ה-app signing key (שני fingerprints במערך).
שלב 3: קליטה ב-MainActivity
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.OS;
namespace ShopMaui;
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true,
LaunchMode = LaunchMode.SingleTask,
ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation
| ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize
| ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity
{
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
HandleIntent(Intent);
}
protected override void OnNewIntent(Intent? intent)
{
base.OnNewIntent(intent);
HandleIntent(intent);
}
private static void HandleIntent(Intent? intent)
{
if (intent?.Action == Intent.ActionView && intent.Data is not null)
{
var url = intent.Data.ToString();
_ = DeepLinkRouter.HandleAsync(url!);
}
}
}
קליטה וניתוב של קישורים נכנסים בקוד
שני ה-platform handlers מפנים ל-DeepLinkRouter.HandleAsync. זו הכיתה שממירה URL חיצוני ל-Shell route פנימי. שים אותה בפרויקט המשותף כך שגם iOS וגם Android ישתמשו באותה לוגיקה. גישה זו משתלבת היטב עם הגישה שהצגתי במאמר על MVVM ב-.NET MAUI עם CommunityToolkit.Mvvm, מכיוון שה-ViewModels יכולים לקבל את הפרמטרים דרך QueryProperty attributes.
using System.Web;
namespace ShopMaui.Navigation;
public static class DeepLinkRouter
{
public static async Task HandleAsync(string url)
{
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
return;
// Wait until Shell is ready — the link can arrive before UI initializes
await WaitForShellAsync();
var path = uri.AbsolutePath.Trim('/');
var query = HttpUtility.ParseQueryString(uri.Query);
var route = path switch
{
var p when p.StartsWith("product/") => MapProduct(p, query),
var p when p.StartsWith("order/") => MapOrder(p, query),
"settings/notifications" => "//main/settings/notifications",
_ => "//main"
};
await MainThread.InvokeOnMainThreadAsync(() =>
Shell.Current.GoToAsync(route));
}
private static string MapProduct(string path, System.Collections.Specialized.NameValueCollection q)
{
var id = path.Split('/')[1];
var utm = q["utm_source"] ?? string.Empty;
return $"//main/product?id={Uri.EscapeDataString(id)}&utm={Uri.EscapeDataString(utm)}";
}
private static string MapOrder(string path, System.Collections.Specialized.NameValueCollection q)
{
var id = path.Split('/')[1];
return $"//main/order?id={Uri.EscapeDataString(id)}";
}
private static async Task WaitForShellAsync()
{
for (int i = 0; i < 50 && Shell.Current is null; i++)
await Task.Delay(100);
}
}
ה-WaitForShellAsync נראה מכוער, אני יודע. אבל הוא פותר בעיה אמיתית: כאשר האפליקציה נפתחת מ-cold start דרך deep link, ה-intent או ה-user activity מגיעים לפני ש-Shell.Current הוקם. בלי המתנה, ה-GoToAsync יזרוק NullReferenceException וההודעה תיעלם. מ-.NET 9 יש שיפור בזמן ההפעלה, אבל התופעה עדיין קיימת.
העברת פרמטרים דרך deep links
אחרי ש-Shell מנווט למסך, ה-ViewModel צריך לקבל את הפרמטרים. הדרך הרשמית ב-MAUI היא [QueryProperty]:
הערה חשובה: Shell תמיד מפרק את ה-query string לפני שהוא מגדיר את ה-properties, לכן ערכים כמו %20 מגיעים ל-ViewModel כטקסט רגיל עם רווח. אם ה-URL המקורי הכיל תווים ייחודיים, וודא שקידדת אותם בקוד ה-router (השתמשתי ב-Uri.EscapeDataString).
לפרמטרים מורכבים יותר (JSON, אובייקטים) עדיף להעביר רק מזהה ולטעון את הנתונים מ-API או קאש מקומי בתוך ה-ViewModel. זה שומר את ה-URLs קצרים, קריאים, ובטוחים מפני manipulation. אם אתה בונה אפליקציה שמנטרת אירועי analytics על נחיתות deep link, תוסיף לוגיקה של telemetry ב-DeepLinkRouter. זה מקום טוב לרכז אירועי DeepLinkOpened ולהעביר ל-App Insights, Firebase Analytics או Segment. גישה כזו גם מקלה על כיסוי במסגרת אסטרטגיית בדיקות ל-.NET MAUI 10, כי הרואטר עצמו הופך לכיתה נקייה, ללא תלות ב-UI, שקל לכתוב לה unit tests.
בדיקה במסופים ובמכשירים אמיתיים
בדיקה של deep links במצב אמת דורשת כלים שונים ב-iOS וב-Android. אני שומר את שתי הפקודות הבאות ב-alias של zsh, כדי לחסוך זמן בכל פעם שאני בודק תרחיש חדש.
שים לב: אם המשתמש לחץ פעם אחת "Open in Safari" בעבר על אותו domain, iOS זוכר את הבחירה ולא יפתח את האפליקציה יותר. פתרון: החלק על הבאנר של הקישור בסאפרי מטה כדי לאלץ פתיחה באפליקציה, או מחק את האפליקציה ותתקין מחדש כדי לאפס את הבחירה. Apple מכנה זאת "Universal Link opt-out" ואין API לאפס אותו תוכניתית.
Android Emulator / מכשיר
# App Link
adb shell am start -W -a android.intent.action.VIEW \
-d "https://shop.example.com/product/42?utm_source=email" \
com.example.shopmaui
# Verify autoVerify status
adb shell pm get-app-links com.example.shopmaui
# Force re-verification
adb shell pm verify-app-links --re-verify com.example.shopmaui
הפקודה השנייה (get-app-links) מציגה את הסטטוס של כל דומיין שהאפליקציה מבקשת לאמת. הערכים הרלוונטיים הם verified (הצלחה), none (עוד לא נבדק), או שגיאה ספציפית. אם רואים 1024, זה אומר שהאימות נכשל בגלל fingerprint לא תואם או קובץ assetlinks.json לא נגיש. הרצת הפקודה השלישית מכריחה אימות מחדש בלי צורך להתקין את האפליקציה מחדש.
מלכודות נפוצות ואיך להימנע מהן
הקובץ AASA לא נטען
Apple מטמיע את הקובץ פעם אחת בהתקנה. אם שינית אותו וזה לא עובד, בדוק שלושה דברים: (1) הכתובת https://your-domain.com/.well-known/apple-app-site-association מחזירה 200 עם Content-Type: application/json; (2) הקובץ הוא JSON תקין ללא BOM; (3) האפליקציה חתומה עם ה-Team ID שרשום ב-appIDs. הכלי הרשמי Apple's App Search API Validation Tool יראה לך מה Apple רואה בפועל, וזה הכלי הראשון שאני מריץ כשמשהו לא עובד.
App Link פותח את הדפדפן במקום האפליקציה
הסיבה השכיחה: android:autoVerify="true" חסר, או שהאימות נכשל. הרץ adb shell pm get-app-links your.package ובדוק את הסטטוס. אם הסטטוס הוא legacy_failure, בדרך כלל מדובר ב-fingerprint לא נכון או ש-assetlinks.json מוחזר עם content-type שגוי (למשל text/plain במקום application/json). נעילה של השרת ל-HTTPS בלבד: App Links לא תעבור אימות דרך HTTP.
Cold start פותח לדף הבית במקום ליעד
זו הבעיה של Shell.Current שעוד לא הוקם. הפתרון הוא ה-WaitForShellAsync שהראיתי, או להזרים את ה-URL דרך SecureStorage ולעבד אותו ב-App.xaml.cs בתוך OnStart. אני מעדיף את הגישה הראשונה כי היא לא דורשת state חיצוני, וקל יותר לדבג אותה.
Query parameters נעלמים
אם ה-QueryProperty לא מתמלא, ודא שאתה מפרסם את ה-URL עם & encoded נכון כשהוא מגיע מ-HTML, ושהפרמטר שאתה מקבל בקוד תואם לשם ה-attribute (case-sensitive). Shell לא מתלונן על שגיאות פרמטרים, הוא פשוט משאיר את ה-property כ-null. זה סוג של debugging שיכול לגזול חצי יום עד שמסתכלים על ה-source of truth.
מלכודת ההגירה מ-Xamarin.Forms
ב-Xamarin.Forms הרבה אפליקציות הסתמכו על custom URI schemes ועל App.OnAppLinkRequestReceived. ב-MAUI ה-API הזה עדיין קיים אבל לא מומלץ; Shell הוא הדרך המודרנית. אם אתה עובר מ-Xamarin, כדאי לשלב את החזרה על הלוגיקה במסגרת מסלול ההגירה המלא מ-Xamarin.Forms ל-.NET MAUI 10 ולא לנסות לעשות זאת כמשימה נפרדת. חוסך הרבה כאב ראש בהמשך.
שאלות נפוצות
מה ההבדל בין Universal Links ל-App Links?
Universal Links הם המנגנון של Apple ל-iOS, מבוססים על קובץ apple-app-site-association ואימות של Team ID. App Links הם המקבילה של Google ל-Android, מבוססים על assetlinks.json ואימות של SHA-256 fingerprint. שניהם משתמשים ב-URLs רגילים של HTTPS, אבל ה-verification chain שונה לחלוטין.
האם .NET MAUI תומך ב-custom URI schemes?
כן, הן דרך [IntentFilter] ב-MainActivity ב-Android, והן דרך CFBundleURLTypes ב-Info.plist ב-iOS. עם זאת, גם Apple וגם Google ממליצות להעדיף Universal Links/App Links על פני custom schemes בשימושים ציבוריים, בעיקר בגלל שמעולם לא הייתה למערכת ההפעלה דרך למנוע שתי אפליקציות מלרשום את אותה סכימה.
איך בודקים Universal Links ב-iOS Simulator?
עם xcrun simctl openurl booted "https://your-domain.com/path". הפקודה מדמה קליק על קישור מאפליקציה חיצונית. חשוב להתקין את האפליקציה על ה-simulator לפחות פעם אחת כדי ש-iOS יוריד את קובץ AASA. Apple לא מורידה אותו מיידית לאחר ההתקנה, אלא בתוך דקות ספורות ברקע.
למה App Link שלי לא פותח את האפליקציה ב-Android 12?
ב-Android 12 (API 31) Google הידקה את דרישות האימות. אם autoVerify="true" לא מוגדר או שהאימות נכשל, המערכת לא תפתח את האפליקציה גם אם המשתמש רוצה. הרץ adb shell pm get-app-links your.package וודא שהסטטוס הוא verified. אם לא, בדוק שקובץ assetlinks.json נגיש דרך HTTPS ומכיל את ה-fingerprint הנכון.
האם צריך שרת ייעודי כדי לארח את קבצי האימות?
לא. כל שרת HTTPS שהוא ה-origin של הדומיין שלך יעבוד. הקבצים חייבים להיות בדיוק ב-/.well-known/apple-app-site-association וב-/.well-known/assetlinks.json, מוגשים עם MIME type application/json, ובלי redirect (אחרת iOS ידחה את הקובץ). אפשר להשתמש ב-Cloudflare Pages, Netlify, S3 עם CloudFront, או כל CDN אחר.
איך מעבירים פרמטרים מורכבים דרך deep link?
העבר רק מזהים בסיסיים דרך ה-URL, וטען את ה-payload המלא בתוך ה-ViewModel מ-API או מקאש מקומי. זה שומר על URLs קצרים, מונע חשיפת מידע רגיש, ומבטיח שהאפליקציה תוכל להתאושש גם אם הפרמטרים בוצעו manipulation. לפרמטרים לא רגישים כמו utm_source, השתמש ב-query string רגיל עם [QueryProperty] ב-ViewModel.