Deep Linking و Universal Links در .NET MAUI: راهنمای کامل پیادهسازی در ۲۰۲۶
در این راهنما هر سه مدل Deep Linking در .NET MAUI 9 یعنی Custom URL Scheme، Universal Links در iOS و Android App Links را با کد قابلاجرا و تست عملی پیادهسازی میکنیم.
Deep Linking در .NET MAUI به اپلیکیشن شما اجازه میدهد تا با کلیک روی یک URL (چه از مرورگر، چه از پیامک یا ایمیل) مستقیماً به یک صفحهی مشخص داخل اپ منتقل شود، بدون اینکه کاربر مجبور باشد از Home Screen مسیر را دستی طی کند. در iOS این کار با Universal Links و در اندروید با Android App Links انجام میشود، و هر دو نیازمند یک فایل تأیید دامنه (AASA یا assetlinks.json) روی سرور شما هستند. خب، در این راهنما هر سه مدل، یعنی Custom URL Scheme قدیمی، Universal Links و App Links مدرن، را با کد قابلاجرا برای .NET MAUI 9 پیادهسازی میکنیم.
Universal Links (iOS) و App Links (Android) دو استاندارد رسمی برای Deep Linking مبتنی بر HTTPS هستند که بر خلاف Custom URL Scheme قابل جعل نیستند.
برای فعالسازی Universal Links به فایل apple-app-site-association در مسیر /.well-known/ دامنهتان نیاز دارید که با Content-Type=application/json سرو شود.
برای App Links اندروید فایل assetlinks.json + اعلان autoVerify="true" در Intent Filter لازم است؛ از Android 12 به بعد تأیید دامنه سختگیرانهتر شده است.
در .NET MAUI لینک ورودی را در App.xaml.cs از طریق Shell.Current.GoToAsync به مسیر مناسب route میکنیم.
تست Deep Link روی شبیهساز با xcrun simctl openurl برای iOS و adb shell am start -a android.intent.action.VIEW -d برای اندروید انجام میشود.
سرویسهای جانبی مثل Firebase Dynamic Links (که در ۲۰۲۵ منسوخ شد) و Branch.io فقط برای Deferred Deep Linking منطقی هستند نه Deep Linking ساده.
Deep Link چیست و چه تفاوتی با Universal Link دارد؟
Deep Link یک اصطلاح چتر است برای هر URL که کاربر را به یک محتوای مشخص داخل اپلیکیشن میبرد. اما در عمل سه نوع کاملاً متفاوت داریم که اغلب توسعهدهندهها آنها را با هم اشتباه میگیرند. درک این تفاوت قبل از نوشتن حتی یک خط کد ضروری است؛ چون انتخاب اشتباه میتواند منجر به این شود که لینکهای شما در Safari باز شوند، در WhatsApp بلاک شوند یا توسط بدافزارها هایجک گردند.
Custom URL Scheme قدیمیترین مدل است: لینکی مثل myapp://product/42. هر اپ میتواند هر scheme دلخواهی ثبت کند و اگر دو اپ scheme یکسانی ثبت کنند، نتیجه روی iOS غیرقابل پیشبینی است. این مدل هیچ ضمانتی برای امنیت ندارد و در ۲۰۲۶ فقط برای OAuth Callback یا ارتباط بین اپهای یک تیم منطقی است.
Universal Links (iOS) و App Links (Android) روی پروتکل HTTPS کار میکنند. لینکی مثل https://shop.example.com/product/42 اگر اپ نصب باشد در اپ باز میشود و در غیر این صورت به وبسایت میرود. این مدل امن است چون سیستمعامل قبل از باز کردن لینک در اپ، فایل تأیید دامنه را روی سرور شما چک میکند؛ بنابراین هیچ اپ دیگری نمیتواند ادعا کند که مالک shop.example.com است.
تجربهی من از سه اپ تولیدی این است: اگر اپ شما کوچک است و فقط نیاز به OAuth Redirect دارد، Custom Scheme کافی است. اما برای هر سناریوی واقعی (اشتراکگذاری محصول، دعوت دوستان، باز کردن سفارش از ایمیل) حتماً به سراغ Universal Links / App Links بروید. هزینهی تنظیم اولیهاش چند ساعت است، ولی صادقانه بگویم هیچوقت بابتش پشیمان نمیشوید.
سه استراتژی Deep Linking در MAUI
قبل از پیادهسازی، بهتر است سه گزینه را در یک جدول مقایسهای کنار هم ببینیم تا تصمیم آگاهانهتری بگیرید. هر سه مدل در .NET MAUI قابل پیادهسازی هستند، اما هزینهی نگهداری و درجهی امنیتشان متفاوت است.
ویژگی
Custom URL Scheme
Universal Links (iOS)
App Links (Android)
فرمت URL
myapp://...
https://domain/...
https://domain/...
تأیید مالکیت دامنه
ندارد
AASA file
assetlinks.json
Fallback به وب
خطای 404 یا alert
خودکار در Safari
خودکار در Chrome
قابل جعل توسط اپ دیگر
بله
خیر
خیر (با autoVerify)
کار در iMessage / Mail
محدود
کامل
ندارد
زمان راهاندازی
۱۰ دقیقه
۱–۲ ساعت
۱–۲ ساعت
توصیه برای ۲۰۲۶
فقط OAuth
اجباری
اجباری
پیادهسازی Universal Links در iOS با .NET MAUI
برای فعالسازی Universal Links سه قطعهی پازل لازم است: فایل apple-app-site-association روی سرور، Entitlement مناسب در پروژهی MAUI و کد دریافتکننده در AppDelegate. مرحلهبهمرحله جلو میرویم تا هیچ نکتهای از قلم نیفتد.
گام ۱: ساخت فایل AASA
فایلی به نام apple-app-site-association (بدون پسوند) بسازید و دقیقاً در مسیر https://yourdomain.com/.well-known/apple-app-site-association قرار دهید. این فایل باید با Content-Type: application/json و بدون redirect سرو شود؛ در غیر این صورت Apple آن را نمیپذیرد.
ABCDE12345 همان Team ID شما در Apple Developer Portal است و com.yourcompany.yourapp برابر Bundle ID اپ. صحت فایل را با ابزار رسمی Apple بررسی کنید: AASA Validator یا با دستور curl -I https://yourdomain.com/.well-known/apple-app-site-association.
گام ۲: تنظیم Entitlements در پروژه MAUI
در پوشهی Platforms/iOS فایلی به نام Entitlements.plist ایجاد یا ویرایش کنید:
در Platforms/iOS/AppDelegate.cs متد ContinueUserActivity را override کنید:
using Foundation;
using UIKit;
namespace YourApp;
[Register("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 { } url)
{
// ارسال به منطق مرکزی روتینگ که در App.xaml.cs تعریف شده
MainThread.BeginInvokeOnMainThread(async () =>
{
await DeepLinkRouter.HandleAsync(url.AbsoluteString);
});
return true;
}
return false;
}
}
پیادهسازی Android App Links در .NET MAUI
در اندروید فرآیند مشابه است اما با ابزارهای متفاوت. فایل assetlinks.json باید روی دامنه باشد، Intent Filter در MainActivity با autoVerify="true" اعلام شود و کد دریافتکننده در OnNewIntent پیادهسازی گردد. برای پروژههای بزرگ پیشنهاد میکنم همراه با این مقاله، راهنمای ما دربارهی معماری MVVM و Shell Navigation در .NET MAUI را هم مرور کنید تا منطق روتینگ را تمیزتر سازماندهی کنید.
گام ۱: تولید assetlinks.json
قبل از هر چیز SHA-256 fingerprint کلید امضای اپ خود را به دست آورید. اگر از Play App Signing استفاده میکنید، این مقدار را از کنسول Google Play، بخش Setup → App Integrity کپی کنید. برای Debug Keystore محلی:
نکتهی مهم (و من خودم اولین بار با همین گیر کردم): از Android 12 (API 31) به بعد اگر autoVerify="true" باشد ولی فایل assetlinks.json در دسترس نباشد یا fingerprint مطابقت نکند، سیستم بهجای باز کردن اپ، لینک را در مرورگر باز میکند، آن هم بدون هیچ هشدار به کاربر. برای دیباگ از دستور زیر کمک بگیرید:
تا اینجا لینک ورودی را دریافت کردهایم اما هنوز کاربر را به صفحهی درست نبردهایم. در .NET MAUI رایجترین الگو استفاده از Shell.Current.GoToAsync با Route Parameter است. یک سرویس مرکزی به نام DeepLinkRouter طراحی میکنیم که از هر دو پلتفرم فراخوانی شود و تمام منطق routing را در یکجا متمرکز کند.
public static class DeepLinkRouter
{
public static string? PendingLink { get; set; }
public static async Task HandleAsync(string url)
{
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
return;
// اگر Shell هنوز آماده نیست، لینک را در صف نگه میداریم
if (Shell.Current is null)
{
PendingLink = url;
return;
}
var segments = uri.AbsolutePath.Trim('/').Split('/');
switch (segments)
{
case ["product", var id]:
await Shell.Current.GoToAsync(
$"//product?id={Uri.EscapeDataString(id)}");
break;
case ["order", var id]:
await Shell.Current.GoToAsync(
$"//order?id={Uri.EscapeDataString(id)}");
break;
case ["invite", var code]:
await Shell.Current.GoToAsync(
$"//signup?invite={Uri.EscapeDataString(code)}");
break;
default:
// لینک ناشناخته: به صفحه اصلی fallback میکنیم
await Shell.Current.GoToAsync("//home");
break;
}
}
}
در صفحهی مقصد پارامتر را با QueryProperty دریافت کنید:
[QueryProperty(nameof(ProductId), "id")]
public partial class ProductPage : ContentPage
{
public string ProductId
{
set => ((ProductViewModel)BindingContext).LoadProduct(value);
}
public ProductPage(ProductViewModel vm)
{
InitializeComponent();
BindingContext = vm;
}
}
اگر اپ شما حالت آفلاین دارد و میخواهید Deep Linkها حتی بدون اتصال شبکه کار کنند، الگوهای ذکرشده در معماری Offline-First در .NET MAUI به شما کمک میکنند تا دادههای لازم را قبل از اینکه کاربر متوجه قطع شبکه شود از کش محلی بارگذاری کنید.
چگونه Deep Linkها را تست کنیم؟
یکی از پرتکرارترین سؤالاتی که در پروژهها میبینم این است: «چرا لینکم وقتی روی Notes میچسبانم و کلیک میکنم در Safari باز میشود؟». پاسخ معمولاً این است: یا AASA file در دسترس نیست، یا Apple آن را هنوز کش نکرده، یا کاربر یکبار «Open in Safari» را انتخاب کرده و iOS ترجیح را به یاد سپرده است. تست منظم پیش از انتشار، ۹۰٪ این مشکلات را حذف میکند.
تست iOS با شبیهساز
# باز کردن یک URL در شبیهساز بوتشده
xcrun simctl openurl booted "https://yourdomain.com/product/42"
# لیست شبیهسازها
xcrun simctl list devices
# بررسی اینکه AASA file توسط iOS دانلود شده
xcrun simctl spawn booted log stream --predicate \
'subsystem == "com.apple.swcd"' --info
تست اندروید با ADB
# شبیهسازی کلیک روی Deep Link
adb shell am start -a android.intent.action.VIEW \
-d "https://yourdomain.com/product/42" \
com.yourcompany.yourapp
# بررسی وضعیت تأیید App Links
adb shell pm get-app-links com.yourcompany.yourapp
# پاک کردن ترجیح کاربر (اگر یکبار "Always open in browser" را زده)
adb shell pm reset-app-links com.yourcompany.yourapp
برای تست end-to-end توصیه میکنم لینکها را در یک پیام Slack یا Telegram به خودتان بفرستید و از همانجا کلیک کنید. این محیط بسیار به سناریوی واقعی کاربر نزدیکتر از کپیپیست در Safari است، چون iOS رفتار متفاوتی برای لینکهای داخل اپهای پیامرسان دارد. برای تستهای اتومیشن، یکپارچهسازی این سناریوها در سوییت Appium شما (همانطور که در راهنمای تستنویسی در .NET MAUI توضیح دادهام) کاملاً منطقی است.
اشتباهات رایج و راهکار رفع آنها
من این الگو را در سه اپ تولیدی پیاده کردهام و در هر سه پروژه با اشتباهات تقریباً یکسانی روبهرو شدم. فهرست زیر بر اساس فراوانی و میزان تأثیر مرتب شده است تا بتوانید مشکلات مشابه را سریعتر تشخیص دهید.
۱. سرو کردن AASA file با Content-Type اشتباه
اگر سرور شما فایل را با text/plain یا application/octet-stream سرو کند، iOS آن را به سکوت نادیده میگیرد. در Nginx این تنظیم را اضافه کنید:
هر مسیری که میخواهید deep link شود باید یا در AASA components یا در Intent Filter ذکر شود. اگر کاربر روی /blog/123 کلیک کند اما فقط /product و /order را ثبت کرده باشید، لینک در مرورگر باز خواهد شد. یا همهی paths را با "/": "*" ثبت کنید (اگر دامنه فقط برای اپ است)، یا فهرست paths را بهروز نگه دارید.
۳. کلید امضای اشتباه در assetlinks.json
اگر شما لوکال build میکنید با Debug keystore اما assetlinks.json را با fingerprint نسخهی Play Store پر کردهاید، روی دستگاه دولوپر کار نمیکند. راهحل: هر دو fingerprint را در آرایهی sha256_cert_fingerprints قرار دهید تا هر دو build شناسایی شوند.
۴. Race condition هنگام Cold Start
وقتی اپ از حالت کاملاً بسته باز میشود، Shell هنوز ساخته نشده ولی Deep Link قبلاً رسیده است. اگر مستقیم Shell.Current.GoToAsync صدا بزنید، NullReferenceException میگیرید. راهحل: لینک را در PendingLink ذخیره کنید و در رویداد Shell.Current.Navigated یا در سازندهی AppShell پردازش کنید.
۵. Universal Link روی همان دامنهی صفحهی فعلی
یک رفتار عجیب iOS این است: اگر کاربر داخل Safari باشد و روی لینکی از همان دامنهی صفحهی فعلی کلیک کند، iOS اپ را باز نمیکند. این یک محدودیت طراحی است نه باگ. برای دور زدن آن، از یک دامنهی جداگانه برای deep linkها استفاده کنید (مثلاً link.yourdomain.com).
آیا برای Universal Links حتماً به دامنهی اختصاصی نیاز دارم؟
بله، Universal Links فقط روی HTTPS کار میکند و نیازمند فایل apple-app-site-association روی دامنهای است که مالک آن هستید. میتوانید از یک سابدامین (مثلاً link.example.com) استفاده کنید اما نمیتوانید از دامنههای اشتراکی مثل GitHub Pages برای پروژهی تولیدی استفاده کنید چون TLS certificate و کنترل .well-known اهمیت دارد.
چرا App Link من بعد از نصب اپ کار نمیکند ولی بعد از verify-app-links کار میکند؟
این مشکل از Android 12 شایع شده است. سیستم در زمان نصب فقط یکبار assetlinks.json را چک میکند و اگر آن لحظه فایل در دسترس نبود یا تأخیر شبکه داشت، verification fail میشود و دیگر تلاش نمیکند. راهحل پایدار، انتشار اپ با autoVerify=true از همان روز اول و اطمینان از پاسخ سریع CDN است.
تفاوت Deferred Deep Linking با Deep Linking معمولی چیست؟
Deep Linking معمولی فرض میکند اپ نصب است. Deferred Deep Linking یعنی کاربر روی لینکی کلیک میکند، اپ نصب نیست، به استور میرود، نصب میکند و اولین بار که اپ را باز میکند مستقیم به همان محتوای اصلی میرود. این قابلیت در iOS و Android بومی پشتیبانی نمیشود و نیاز به سرویسهای جانبی مثل Branch.io یا Adjust دارد.
آیا میتوانم Custom URL Scheme و Universal Links را همزمان داشته باشم؟
بله و در عمل توصیه میکنم همین کار را بکنید. Custom Scheme برای OAuth callback (مثلاً Google Sign-In) و Universal Links برای محتوای قابلاشتراکگذاری. این دو در تعارض نیستند و هر کدام در سناریوی خود بهتر عمل میکنند.
چطور بفهمم کاربر از چه لینکی وارد اپ شده برای تحلیل آماری؟
قبل از فراخوانی Shell.Current.GoToAsync در DeepLinkRouter، URL کامل را به سرویس تحلیلی خود (App Insights، Mixpanel، Firebase Analytics) ارسال کنید با یک event مثل deep_link_opened و پارامتر source_url. سپس میتوانید UTM parameters را پارس کنید و کمپینها را track کنید.
راهنمای عملی ساخت اپلیکیشن موبایل با Blazor Hybrid روی .NET MAUI در ۲۰۲۶: از تنظیم پروژه با dotnet new، دسترسی به دوربین و SecureStorage، تا اشتراک کد با وب و بهینهسازی عملکرد.
پیادهسازی کامل خرید درونبرنامهای در .NET MAUI با StoreKit 2 و Google Play Billing v7؛ از پیکربندی فروشگاهها تا اعتبارسنجی سرور، مدیریت اشتراک و رفع خطاهای رایج، با مثال کد آمادهی تولید.
پشتیبانی Xamarin.Forms پایان یافته. این راهنما با Upgrade Assistant، تبدیل Renderer به Handler و رفع خطاهای رایج، مهاجرت به .NET MAUI ۹ را گامبهگام نشان میدهد.