Deep Linking in .NET MAUI 10: Universal Links, App Links und URI-Schemes richtig einrichten

Deep Linking in .NET MAUI 10 verbindet https-URLs und Custom-Schemes mit Shell-Routen. Der Praxisleitfaden zeigt Universal Links, App Links, apple-app-site-association, assetlinks.json und Deferred Deep Linking auf iOS und Android.

.NET MAUI 10 Deep Linking Guide (2026)

Aktualisiert: 15. Juli 2026

Deep Linking in .NET MAUI 10 ist die Technik, mit der du eine https-URL wie https://deineapp.de/bestellung/42 oder ein Custom-Scheme wie meineapp://order/42 direkt in eine bestimmte Seite deiner nativen App auflösen lässt. Auf iOS heißt das Universal Links, auf Android App Links, und dazu kommt in beiden Welten noch das klassische URI-Scheme. In der Praxis brauchst du drei Bausteine: eine plattformseitige Deklaration im Manifest bzw. in Info.plist, eine signierte Domain-Verifizierung (apple-app-site-association bzw. assetlinks.json) und einen Handler im MAUI-Code, der die URL an dein Shell-Routing weiterreicht.

  • Universal Links (iOS) und App Links (Android) brauchen zwingend HTTPS und eine öffentlich erreichbare Verifizierungs-Datei auf deiner Domain, sonst öffnet das System immer den Browser.
  • Custom URI-Schemes (meineapp://) funktionieren ohne Domain, sind aber nicht mehr klickbar aus Mail und Messengern und sollten nur als Fallback dienen.
  • In .NET MAUI 10 fängst du eingehende URLs zentral in App.xaml.cs über OnAppLinkRequestReceived ab und mappst sie mit Shell.Current.GoToAsync auf eine Route.
  • Auf iOS muss die App Bundle-ID im AASA-File mit einem konkreten Path-Pattern verknüpft werden, auf Android muss autoVerify="true" im Intent-Filter gesetzt sein.
  • Ohne digital asset links laufen Android App Links seit Android 12 nur noch als Standard-Browser-Öffnung. Das ist das häufigste Debugging-Symptom.
  • Deferred Deep Linking (Link öffnet App-Store, App merkt sich die Ziel-URL) musst du über Firebase-Dynamic-Links-Nachfolger oder eigene Backend-Logik lösen. MAUI liefert dafür keinen Out-of-the-Box-Mechanismus.

Was ist Deep Linking und welche Arten gibt es?

Deep Linking bezeichnet jede URL, die nicht auf einer Startseite landet, sondern eine spezifische Ansicht in einer nativen App öffnet. Historisch begann das mit URI-Schemes wie fb://profile/123, die Apple und Google später als unsicher und leicht kaperbar eingestuft haben, weil sich jede App für ein beliebiges Scheme registrieren konnte. Deshalb wurden Universal Links (iOS 9, 2015) und App Links (Android 6, ebenfalls 2015) eingeführt. Sie binden die App an eine verifizierte HTTPS-Domain und lösen das Kaperproblem, weil nur der Domain-Betreiber die Verifizierungs-Datei aufspielen kann.

In der Praxis brauchst du in einer MAUI-App fast immer beides: Universal- bzw. App-Links für alles, was aus einer E-Mail oder einem QR-Code kommt, und ein Custom URI-Scheme für OAuth-Callback-Flows sowie Test-Szenarien, in denen du keine erreichbare Domain hast. Ich habe das in drei produktiven Apps ausgerollt, und jedes Mal war die Domain-Verifizierung der Teil, an dem 90 % der Zeit hängen blieb. Der Code in MAUI selbst? Ehrlich gesagt überraschend wenig.

Neben diesen drei Grundformen gibt es noch Deferred Deep Linking, also das Öffnen einer Ziel-URL, obwohl die App im Moment des Klicks noch nicht installiert war. Das ist ein separates Problem, das MAUI nicht direkt löst. Du brauchst dafür entweder Firebase-Nachfolgetechnologie, AppsFlyer oder eine eigene Backend-Attribution über die IP-Adresse (bzw. ein First-Party-Cookie beim Store-Absprung).

Beide Technologien machen aus einer normalen https-URL einen Link, der die App statt des Browsers öffnet, sofern die App installiert ist. Der zentrale Unterschied liegt in der Verifizierungs-Datei und im Verhalten, wenn die App nicht installiert ist.

MerkmaliOS Universal LinksAndroid App LinksURI-Scheme (beide)
ProtokollNur HTTPSNur HTTPSBeliebig (z. B. meineapp://)
Verifizierungs-Datei/.well-known/apple-app-site-association/.well-known/assetlinks.jsonKeine
Signierung erforderlichJSON, HTTPS-Cert reichtSHA256-Fingerprint des Signing-KeysNein
Fallback ohne AppÖffnet WebsiteÖffnet WebsiteZeigt Fehler oder gar nichts
Anti-HijackingJa (Domain-Bindung)Ja (seit Android 12 verpflichtend)Nein, jede App darf sich registrieren
Aus E-Mail klickbarJaJaNein (viele Clients blockieren es)
Testen im SimulatorNur begrenzt, echte Domain nötigVoll möglich mit adbVoll möglich

Praktisch heißt das: Wenn du eine E-Mail-Kampagne mit personalisierten Deep Links planst, führt an Universal und App Links kein Weg vorbei. Brauchst du dagegen einen OAuth-Redirect und die Nutzer sollen keine erste E-Mail sehen? Dann ist ein URI-Scheme sauberer, weil es keine Domain-Verifizierung braucht. Und ja, du kannst beides parallel in derselben App haben. Das ist sogar der Normalfall.

Custom URI-Scheme in .NET MAUI einrichten

Ein Custom URI-Scheme ist der einfachste Einstieg, weil er keine Domain-Konfiguration braucht. Der Preis: keine Anti-Hijacking-Garantie und in vielen Kontexten (E-Mail-Clients, iMessage-Vorschauen) nicht klickbar. Verwende es für OAuth-Callbacks, interne Tests und als Fallback, wenn die Universal-Link-Verifizierung fehlschlägt.

iOS: URL-Scheme in Info.plist registrieren

Öffne Platforms/iOS/Info.plist und ergänze den Block CFBundleURLTypes. Wähle ein Scheme, das eindeutig zu deiner App gehört (Reverse-DNS-Notation empfohlen), sonst kann eine andere App den Link abfangen.

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLName</key>
        <string>de.deinefirma.meineapp</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>meineapp</string>
        </array>
    </dict>
</array>

Android: Intent-Filter im Manifest ergänzen

Öffne Platforms/Android/AndroidManifest.xml und ergänze innerhalb der MainActivity-Deklaration einen Intent-Filter. Alternativ kannst du das per [IntentFilter]-Attribut direkt an die MainActivity-Klasse hängen. Ich bevorzuge das in meinen Projekten, weil es näher am Code liegt.

[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true,
    LaunchMode = LaunchMode.SingleTop, /* ... */)]
[IntentFilter(new[] { Intent.ActionView },
    Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
    DataScheme = "meineapp")]
public class MainActivity : MauiAppCompatActivity { }

Universal Links binden deine App an eine verifizierte HTTPS-Domain. Der Ablauf hat drei Schritte, die alle stimmen müssen, sonst öffnet iOS still den Browser statt der App.

1. Associated Domains im Entitlements-File aktivieren

Erstelle Platforms/iOS/Entitlements.plist, falls sie noch nicht existiert, und ergänze:

<key>com.apple.developer.associated-domains</key>
<array>
    <string>applinks:deineapp.de</string>
    <string>applinks:www.deineapp.de</string>
</array>

Danach im .csproj sicherstellen, dass die Entitlements für iOS-Release-Builds verwendet werden: <CodesignEntitlements>Platforms/iOS/Entitlements.plist</CodesignEntitlements> innerhalb einer iOS-PropertyGroup.

2. apple-app-site-association hosten

Auf https://deineapp.de/.well-known/apple-app-site-association muss diese Datei liegen (ohne Datei-Endung, ausgeliefert mit Content-Type: application/json, ohne Redirects und unter gültigem TLS-Zertifikat):

{
  "applinks": {
    "details": [
      {
        "appIDs": ["TEAMID.de.deinefirma.meineapp"],
        "components": [
          { "/": "/bestellung/*", "comment": "Bestellungs-Detail" },
          { "/": "/produkt/*",    "comment": "Produkt-Detail" },
          { "/": "/", "exclude": true }
        ]
      }
    ]
  }
}

TEAMID findest du im Apple Developer Portal unter Membership. Die appIDs muss exakt mit deiner Bundle-ID übereinstimmen, Groß-/Kleinschreibung inklusive. Prüfe die Datei mit curl -I https://deineapp.de/.well-known/apple-app-site-association und dem AASA-Validator von Branch. Für die vollständige JSON-Spezifikation lohnt sich ein Blick in Apples offizielle Dokumentation zu Associated Domains.

3. Link im MAUI-Code empfangen

Ein empfangener Universal Link landet als NSUserActivity beim MauiAppDelegate. Die Standard-Implementierung leitet ihn automatisch an Application.Current.SendOnAppLinkRequestReceived weiter, du kannst also direkt in App.xaml.cs lauschen. Details dazu findest du in unserem Praxisartikel zu Shell-Routing und Navigation in .NET MAUI.

Auf Android musst du drei Dinge tun: den Intent-Filter im Manifest mit autoVerify="true" markieren, den SHA256-Fingerprint deines Signing-Keys ermitteln und eine assetlinks.json-Datei auf deiner Domain hosten. Google beschreibt den Ablauf im Detail in seiner offiziellen App-Links-Dokumentation.

1. Intent-Filter mit autoVerify

[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true,
    LaunchMode = LaunchMode.SingleTop)]
[IntentFilter(new[] { Intent.ActionView },
    AutoVerify = true,
    Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
    DataScheme = "https",
    DataHost = "deineapp.de",
    DataPathPrefix = "/bestellung")]
[IntentFilter(new[] { Intent.ActionView },
    AutoVerify = true,
    Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
    DataScheme = "https",
    DataHost = "deineapp.de",
    DataPathPrefix = "/produkt")]
public class MainActivity : MauiAppCompatActivity { }

2. SHA256-Fingerprint des Signing-Keys ermitteln

Für Debug-Builds:

keytool -list -v -keystore ~/.android/debug.keystore \
        -alias androiddebugkey -storepass android -keypass android

Für Release-Builds musst du den Fingerprint deines Upload-Keys, und falls du Google Play App Signing nutzt zusätzlich den App-Signing-Fingerprint aus der Play Console (unter Setup → App-Integrität), hinzufügen. Ohne den Play-Signing-Fingerprint verifizieren die produktiven Links auf Nutzergeräten nicht, obwohl sie lokal funktionieren. Das ist der wahrscheinlich häufigste Fehler beim Rollout.

3. assetlinks.json hosten

Auf https://deineapp.de/.well-known/assetlinks.json:

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "de.deinefirma.meineapp",
    "sha256_cert_fingerprints": [
      "14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5",
      "AA:BB:CC:...:release-key-fingerprint..."
    ]
  }
}]

Prüfe die Verifizierung auf einem angeschlossenen Gerät mit:

adb shell pm get-app-links de.deinefirma.meineapp

Ein Domain verification state: verified heißt, alles ist grün. Bei legacy_failure oder 1024:none stimmt der Fingerprint oder der Package-Name nicht.

Deep Link an Shell-Routing weiterleiten

Sobald der Link im MAUI-Layer angekommen ist, willst du ihn typischerweise auf eine Shell-Route mappen. .NET MAUI 10 ruft dafür Application.OnAppLinkRequestReceived auf, egal ob der Link von iOS Universal Links, Android App Links oder einem Custom URI-Scheme kam.

public partial class App : Application
{
    public App(IServiceProvider services)
    {
        InitializeComponent();
        _services = services;
    }

    private readonly IServiceProvider _services;

    protected override void OnAppLinkRequestReceived(Uri uri)
    {
        base.OnAppLinkRequestReceived(uri);

        // Beispiel: https://deineapp.de/bestellung/42
        //          meineapp://order/42

        var route = uri.Host switch
        {
            "deineapp.de" or "www.deineapp.de" => MapHttpsPath(uri.AbsolutePath),
            "order"                             => $"//OrderDetailPage?id={uri.Segments.LastOrDefault()}",
            _                                   => "//MainPage"
        };

        // Shell existiert erst, wenn das Fenster gestartet ist.
        // Deshalb Dispatcher, um die Navigation zu verzoegern.
        Dispatcher.Dispatch(async () =>
        {
            if (Shell.Current is null) return;
            await Shell.Current.GoToAsync(route);
        });
    }

    private static string MapHttpsPath(string path)
    {
        // /bestellung/42 -> //OrderDetailPage?id=42
        var segments = path.Trim('/').Split('/');
        return segments switch
        {
            ["bestellung", var id] => $"//OrderDetailPage?id={id}",
            ["produkt", var id]    => $"//ProductDetailPage?id={id}",
            _                       => "//MainPage"
        };
    }
}

Wichtig: Wenn die App gerade erst kalt gestartet wird, feuert das Event, bevor Shell existiert. Der Dispatcher.Dispatch-Callback im obigen Beispiel puffert die Navigation, bis das Fenster fertig ist. In der Praxis reicht das für 95 % der Fälle. Bei komplexeren Situationen (Login-Pflicht, Modal-Stapel, Sync-Vorgänge im Hintergrund) speichere die Ziel-Route in einem Service und navigiere aus deinem AppShell-Konstruktor heraus, nachdem alle Voraussetzungen erfüllt sind.

Für Custom URI-Schemes und Android App Links funktioniert der Kommandozeilen-Weg zuverlässig. Für iOS Universal Links brauchst du in der Regel ein echtes Gerät und die vollständig gehostete AASA-Datei.

Android

# URI-Scheme
adb shell am start -a android.intent.action.VIEW \
    -d "meineapp://order/42" de.deinefirma.meineapp

# App Link (https)
adb shell am start -a android.intent.action.VIEW \
    -d "https://deineapp.de/bestellung/42"

# Verifizierungs-Status pruefen
adb shell pm get-app-links de.deinefirma.meineapp

# Manuell neu verifizieren lassen (nur Debug)
adb shell pm verify-app-links --re-verify de.deinefirma.meineapp

iOS

# URI-Scheme im Simulator
xcrun simctl openurl booted "meineapp://order/42"

# Universal Link im Simulator (funktioniert nur bei gehosteter AASA)
xcrun simctl openurl booted "https://deineapp.de/bestellung/42"

Ein sehr verlässlicher iOS-Test: Schicke dir die URL in Notes (die Notizen-App) und tippe sie dort an. Safari öffnet Universal Links nach dem ersten Tippen auf die Adressleiste bewusst nicht in der App. Das ist kein Bug, sondern Apples "erste-URL-manuell-eingegeben"-Regel. Aus Notes, Messages oder Mail klappt es dagegen zuverlässig.

Deferred Deep Linking und Attribution

Ein Deferred Deep Link ist ein Link, der die Ziel-Route auch dann öffnet, wenn die App im Moment des Klicks noch nicht installiert war. Nutzer klicken den Link, landen im App-Store, installieren die App, öffnen sie zum ersten Mal, und die App springt trotzdem direkt auf die Detail-Seite, nicht auf den Startscreen.

Bis Anfang 2025 war Firebase Dynamic Links die naheliegende Lösung. Google hat den Dienst am 25. August 2025 endgültig abgeschaltet (siehe Firebase Dynamic Links FAQ). Für .NET MAUI stehen jetzt drei praktikable Wege offen:

  • Kommerzielle Attribution-Anbieter wie AppsFlyer, Adjust oder Branch. Die SDKs binden sich via Bindings ein und liefern die Post-Install-URL in einem Callback. Kosten: ab ca. 500 €/Monat.
  • Eigenes Backend mit fingerprinting-basierter Attribution: Beim Klick loggt dein Server IP, User-Agent und eine Kampagnen-ID. Beim ersten App-Start fragt die App den Server mit ihrer IP und Sprache nach ausstehenden Attributionen. Genauigkeit ca. 70 bis 85 %.
  • Play Install Referrer (nur Android): Der Play Store liefert einen 512-Byte-String beim ersten Start, den du bei der Play Console als Kampagnen-Parameter hinterlegen kannst. Für iOS gibt es kein direktes Äquivalent. SKAdNetwork liefert nur aggregierte, verzögerte Daten, keine Deep Links.

Ich habe die eigene-Backend-Variante in einer Retail-App produktiv im Einsatz. Der Aufwand ist überschaubar (~200 Zeilen Backend-Code plus DSGVO-Prüfung, weil IP-basierte Attribution einer datenschutzkonformen Rechtsgrundlage bedarf), und die Genauigkeit reicht für Kampagnen-Auswertung. Für exakte User-Attribution zu bezahlen (AppsFlyer & Co.) lohnt sich erst ab spürbarem Ad-Spend.

Häufige Fehler und deren Ursachen

Aus drei Rollouts kenne ich die immer gleichen Sackgassen. Die Reihenfolge entspricht der Häufigkeit, mit der ich sie erlebt habe.

  • App Link öffnet trotzdem den Browser. Fast immer stimmt der SHA256-Fingerprint in assetlinks.json nicht mit dem tatsächlich signierten APK überein. Ursache: Google Play App Signing nutzt einen anderen Key als dein Upload-Key, aber du hast nur den Upload-Fingerprint hinterlegt. Beide eintragen.
  • Universal Link öffnet nur beim zweiten Tippen die App. Klassische Long-Press-vs-Tap-Fehldiagnose. Auf iOS gibt es außerdem die Regel: URLs, die in derselben Domain aufgerufen wurden, werden im Browser gehalten. Öffne den Link aus einer anderen App (Notes, Messages).
  • AASA-Datei wird nicht geladen. Prüfe mit curl -sD -, dass Content-Type application/json ist, kein Redirect passiert und keine Authentifizierung nötig ist. Cloudflare "Rocket Loader" oder ähnliche HTML-Injections zerstören die Datei ebenfalls.
  • OnAppLinkRequestReceived wird nie gerufen. Auf Android fehlt entweder AutoVerify=true, oder das DataHost-Attribut hat einen Tippfehler. Auf iOS ist die Bundle-ID im appIDs-Array nicht exakt gleich (Case-Sensitive!) oder das Entitlement wurde nicht ins Provisioning-Profile aufgenommen.
  • App startet zwei Mal beim Öffnen eines Links. LaunchMode.SingleTop vergessen. Ergänze das Attribut an MainActivity.
  • Shell-Navigation ignoriert die Route. Der Link kommt zu früh (vor Shell-Initialisierung). Puffere die Route über einen Dispatcher oder ein Task-Completion-Source, wie im Code-Beispiel oben gezeigt.

Wenn du zusätzlich zu Deep Linking auch Push-Notifications in .NET MAUI einsetzt, prüfe außerdem, dass beide dieselbe Payload-Route erkennen. Notifications enthalten oft eine deepLink-Property, die durch dieselben Handler laufen sollte, damit du keine Duplikat-Logik pflegst.

Häufig gestellte Fragen

Braucht .NET MAUI 10 zusätzliche NuGet-Pakete für Deep Linking?

Nein. Alle nötigen APIs (OnAppLinkRequestReceived, Shell.GoToAsync, Intent-Filter) sind Teil des Basis-Frameworks. Zusatzpakete brauchst du erst bei Attribution (AppsFlyer, Branch) oder wenn du einen Firebase-Dynamic-Links-Nachfolger integrieren willst.

Kann ich Universal Links im iOS-Simulator testen?

Ja, aber nur mit einer öffentlich erreichbaren AASA-Datei. xcrun simctl openurl booted "https://deineapp.de/..." öffnet den Link. Achtung: Nach jedem Simulator-Reset muss die App neu installiert werden, damit iOS die AASA neu holt.

Was mache ich, wenn App Links nach dem Play-Store-Rollout nicht mehr funktionieren?

Fast immer fehlt der App-Signing-Key-Fingerprint (nicht der Upload-Key!) in assetlinks.json. Du findest ihn in der Play Console unter Setup → App-Integrität → App-Signing-Schlüsselzertifikat. Nach Aktualisierung der Datei kann die erneute Verifizierung bis zu 24 Stunden dauern.

Wie leite ich einen Deep Link an eine geschützte Seite mit Login-Pflicht?

Speichere die Ziel-Route in einem PendingNavigationService, navigiere zum Login-Screen und rufe die gespeicherte Route nach erfolgreichem Login mit Shell.Current.GoToAsync erneut auf. So bleibt die eigentliche Deep-Link-Logik von deiner Auth-Logik entkoppelt.

Funktioniert Deep Linking in einer .NET MAUI Blazor Hybrid App?

Ja, mit einem Zusatzschritt: OnAppLinkRequestReceived feuert wie gewohnt, aber du navigierst im BlazorWebView statt via Shell. Verwende NavigationManager.NavigateTo("/route", forceLoad: false) aus einem Blazor-Service, den du im MAUI-Handler auflöst.

Marcus Chen
Über den Autor Marcus Chen

Senior mobile architect with a decade of cross-platform experience. Spent the last five years going deep on .NET MAUI in production.