XDev.Android.ProcessIsolation
1.0.0.1
dotnet add package XDev.Android.ProcessIsolation --version 1.0.0.1
NuGet\Install-Package XDev.Android.ProcessIsolation -Version 1.0.0.1
<PackageReference Include="XDev.Android.ProcessIsolation" Version="1.0.0.1"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="XDev.Android.ProcessIsolation" Version="1.0.0.1" />
<PackageReference Include="XDev.Android.ProcessIsolation"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add XDev.Android.ProcessIsolation --version 1.0.0.1
#r "nuget: XDev.Android.ProcessIsolation, 1.0.0.1"
#:package XDev.Android.ProcessIsolation@1.0.0.1
#addin nuget:?package=XDev.Android.ProcessIsolation&version=1.0.0.1
#tool nuget:?package=XDev.Android.ProcessIsolation&version=1.0.0.1
XDev.Android.ProcessIsolation
Build-time tooling that keeps a .NET Android / MAUI app's isolated child processes
(e.g. Stripe Tap to Pay :stripetaptopay) free of the Mono/.NET runtime.
Why
Android SDKs that require a dedicated secure process — Stripe Terminal Tap to Pay, or anything else built on MPoC-style attestation — fail on a stock .NET MAUI app because:
- .NET Android auto-generates a
MonoRuntimeProvider_NContentProvider for every uniqueandroid:processdeclared in the merged manifest. - ContentProviders load before
Application.onCreate(), so the full .NET runtime ends up in the isolated process. - The SDK's
SecureProcessCheckerseeslibmonosgen-2.0.so,libmonodroid.so, etc., and rejects the environment.
This package bundles the three pieces that together solve that problem:
StripMonoRuntimeProviderTask— an MSBuild task that removes theMonoRuntimeProvider_Nentries targeted at configured isolated processes from the mergedAndroidManifest.xml.NativeAppWrapper.java— a pure-JavaApplicationclass generated at build time so the isolated process can instantiate an Application without triggering .NET class loading.MauiBootstrapper— a C# source file shipped as acontentFileso the main-process MAUI app can still initialize even thoughMauiApplicationhas been replaced with the pure-Java wrapper.
Install
This is a build-time-only tooling package. It MUST be consumed with
PrivateAssets="all" so it does not leak into your NuGet dependency graph.
A complete integration looks like:
<ItemGroup>
<PackageReference Include="XDev.Android.ProcessIsolation" Version="1.0.0" PrivateAssets="all">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<IsolatedAndroidProcess Include=":stripetaptopay" />
</ItemGroup>
Without PrivateAssets="all", any NuGet that references a library using this
package will transitively pull the tooling in.
Optional properties
| Property | Default | Purpose |
|---|---|---|
IncludeMauiBootstrapper |
true |
Include the shipped MauiBootstrapper.cs as a Compile source. Set false for non-MAUI apps or if you want to provide your own initialization. |
Wire up - Consumer integration: process isolation
For any child process subject to MPoC attestation: the attestation rejects any process where the .NET (Mono) runtime is loaded. .NET Android auto-generates a MonoRuntimeProvider_N ContentProvider for every unique android:process in the merged manifest, which would otherwise pull Mono into the isolated process and fail the check.
The wrapper itself contains no integration glue — the host app must wire up the XDev.Android.ProcessIsolation build-time package, which strips the auto-injected MonoRuntimeProvider_N entries, supplies a pure-Java NativeAppWrapper Application class, and ships a MauiBootstrapper that re-runs MAUI's init chain from the main Activity. See the package README for the full background.
Consumers need the same four pieces:
csproj package reference + isolated-process declarations (note
PrivateAssets="all"— this is build-time tooling):<PackageReference Include="XDev.Android.ProcessIsolation" Version="0.0.1-alpha008"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> <IsolatedAndroidProcess Include=":[your-isolated-process-name]" />AndroidManifest — swap the auto-injected C#
MauiApplicationACW name for the pure-Java wrapper:<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" ...> <application android:name="xdev.android.processisolation.NativeAppWrapper" tools:replace="android:name" ...>MainActivity.OnCreate — initialize MAUI from the Activity before
base.OnCreate(), because the Application class is no longer aMauiApplication:protected override void OnCreate(Bundle savedInstanceState) { XDev.Android.ProcessIsolation.MauiBootstrapper.BuildMauiApp = builder => { // Any service registrations that used to live in MauiApplication.CreateMauiApp() // belong here — builder.Services is fully available. return MauiProgram.CreateMauiApp(builder); }; XDev.Android.ProcessIsolation.MauiBootstrapper.Initialize(this.Application); base.OnCreate(savedInstanceState); // ... rest of OnCreate ... }Retire any C#
[Application]class that previously extendedMauiApplication. The pure-JavaNativeAppWrapperis now the Application class; a competing C#[Application]will be auto-registered by .NET Android and (withouttools:replace) win the merge.
Linker note (Release builds): NativeAppWrapper is kept by D8/R8 because it's referenced from the manifest, and MauiBootstrapper is kept by the managed linker because the host app calls it directly from MainActivity.OnCreate. This is safe at the default AndroidLinkMode=SdkOnly. If the consumer app sets AndroidLinkMode=Full, the linker may also trim internal MAUI types used by MauiBootstrapper.Initialize (e.g. MauiApplication.ActivityLifecycleCallbacks); a [DynamicDependency] or linker descriptor will be required in that scenario.
Verifying the fix
After a build, inspect the merged manifest to confirm the strip worked:
$ grep -A1 'MonoRuntimeProvider' obj/Debug/net8.0-android/android/AndroidManifest.xml
Only the non-numbered MonoRuntimeProvider (main process) should remain.
Any MonoRuntimeProvider_N targeting an @(IsolatedAndroidProcess) process
is a bug.
Troubleshooting
Build warning "No components found for isolated process '...'"
The task verifies that the merged manifest has at least one <service>,
<activity>, <provider> (non-Mono), or <receiver> declaring
android:process=":..." for each @(IsolatedAndroidProcess) entry.
If none are found, the AAR that should contribute those components is
probably missing from the build, or the manifest merger did not run.
Check your @(AndroidLibrary) / @(AndroidMavenLibrary) items and rebuild.
Black screen / crash on app launch in the main process
Make sure MauiBootstrapper.Initialize(Application) is called before
base.OnCreate(savedInstanceState) in your main Activity, and that
MauiBootstrapper.BuildMauiApp is set before Initialize.
Background
Why all three pieces (StripMonoRuntimeProviderTask, NativeAppWrapper,
and MauiBootstrapper) are required for isolating child processes from
the .NET runtime.
This applies to any Android SDK that runs components in a dedicated
android:process that must remain free of non-native libraries (e.g.
MPoC secure processes, isolated services, etc.).
Xamarin.Forms vs MAUI boot architecture
MAUI changed where framework initialization happens compared to Xamarin.Forms:
| Application class | Framework init location | |
|---|---|---|
| Xamarin.Forms | Plain Java / stock Application |
Forms.Init() + LoadApplication() in Activity |
| MAUI (normal) | C# MauiApplication |
MauiApplication.OnCreate() |
| Process Isolation fix | Plain Java NativeAppWrapper |
MauiBootstrapper.Initialize() in Activity |
In Xamarin.Forms, the Application class was typically a plain Java class
(or not specified at all), and all framework initialization happened at
the Activity level via Forms.Init(this, bundle) + LoadApplication(new App()).
The Application class had no .NET dependencies.
MAUI moved that initialization up to the Application class via
MauiApplication.OnCreate() — building the DI container, creating
MauiContext, wiring up handlers — all before any Activity starts. This
is architecturally cleaner, but it means the Application class is now a
C# class with native JNI methods, which creates a conflict with any
child process that needs to stay .NET-free.
The process isolation fix is essentially reverting to the Xamarin.Forms
boot pattern — a plain Java Application class with MAUI initialization
deferred to the Activity. MauiBootstrapper has more steps than
Forms.Init() because MAUI's initialization chain is heavier
(MauiContext, SetApplicationHandler, IPlatformApplication.Current,
etc.), but the architectural pattern is the same.
Scenario 1: Normal MAUI app boot (no fixes, main process)
When Android launches your app's main process:
Step 1 — ContentProviders load first (before any user code). Android
reads the manifest, finds all <provider> entries for this process, and
calls attachInfo() on each. The generated MonoRuntimeProvider.java
calls mono.MonoPackageManager.LoadApplication(...), which calls
System.loadLibrary("monosgen-2.0"), System.loadLibrary("monodroid"),
etc. The full Mono/.NET runtime is now loaded in the process.
Step 2 — Android instantiates the Application class. The manifest
points at the C# ACW (MainApplication extends MauiApplication).
Constructor runs mono.MonoPackageManager.setContext(this) (works
because Mono is loaded), then Android calls onCreate() which invokes
n_onCreate() — a native JNI method implemented in libmonodroid.so
that calls into C# MauiApplication.OnCreate().
Step 3 — C# MauiApplication.OnCreate() runs. Builds MauiApp,
creates MauiContext, calls SetApplicationHandler(), sets
IPlatformApplication.Current. MAUI is now initialized.
Step 4 — MainActivity launches, UI renders.
Scenario 2: Isolated child process — no fixes
When an SDK starts a child process (e.g. :secureprocess):
Step 1 — MonoRuntimeProvider_N loads. The .NET Android build
system auto-generates a numbered ContentProvider for each unique
android:process in the manifest:
<provider android:name="mono.MonoRuntimeProvider_1"
android:process=":secureprocess" />
Its attachInfo() runs → loads libmonosgen, libmonodroid, etc.
Mono is now loaded in the child process.
Step 2 — Application class instantiates (same class, new process).
Android uses the same android:name Application class for ALL
processes. The ACW constructor and n_onCreate() run. They work because
Mono was loaded in Step 1.
Step 3 — Security checker rejects. Any security checker that scans
loaded native libraries finds libmonosgen-2.0.so, libmonodroid.so →
rejects as untrusted/non-native software.
Result: child process functionality fails.
Scenario 3: Isolated child process — strip MonoRuntimeProvider only
The MSBuild task removes MonoRuntimeProvider_1 from the manifest. No
NativeAppWrapper.
Step 1 — No ContentProviders for this process. No native libraries
loaded. libmonodroid.so is NOT in memory.
Step 2 — Android instantiates the Application class. Still the C#
ACW. MonoPackageManager.setContext() is a Java method that just stores
a reference, so the constructor likely succeeds.
Step 3 — Android calls onCreate() which invokes n_onCreate() — a
JNI method implemented in libmonodroid.so. But libmonodroid.so was
never loaded (no MonoRuntimeProvider ran).
Result: java.lang.UnsatisfiedLinkError: No implementation found for void ...MauiApplication.n_onCreate().
The child process crashes. The SDK's services and activities in that
process can't start. The feature is completely broken, not just
rejected.
Scenario 4: Isolated child process — both fixes (correct solution)
Step 1 — No ContentProviders for this process.
MonoRuntimeProvider_1 stripped. No native libs loaded.
Step 2 — Android instantiates NativeAppWrapper. Manifest says
android:name="...NativeAppWrapper". This is a pure Java class — no
ACW, no mono.* imports, no native methods. No JNI calls, no Mono
dependencies. Runs cleanly.
Step 3 — Security checker passes. Scans loaded native libraries — finds only standard Android libs. No Mono detected. Passes.
Step 4 — SDK components start normally. Services and activities in the isolated process run in a clean native environment.
But what about the main process?
With NativeAppWrapper as the Application class, the main process
boot changes too:
MonoRuntimeProvider(unnumbered) still runs → loads Mono.NativeAppWrapper.onCreate()runs → just logs "Main process".- But
MauiApplication.OnCreate()never runs → no MauiApp, no MauiContext, noSetApplicationHandler(). - MainActivity starts →
MauiAppCompatActivity.OnCreate()→CreatePlatformWindow()→ checksapplication.Handler?.MauiContext→ null → black screen.
This is why MauiBootstrapper exists. It's called from
MainActivity.OnCreate() before base.OnCreate() to manually run the
init chain that MauiApplication.OnCreate() used to handle — the same
pattern Xamarin.Forms used (framework init at the Activity level), just
with MAUI's heavier initialization steps.
Summary
| Fix applied | Isolated child process | Main process |
|---|---|---|
| Neither | Mono loads → security checker rejects | Works normally |
Strip MonoRuntimeProvider only |
UnsatisfiedLinkError → process crash |
Works normally |
NativeAppWrapper only |
MonoRuntimeProvider still loads Mono → rejected |
Black screen (no MAUI init) |
Both + MauiBootstrapper |
No Mono, no crash → clean native process | MAUI inits from Activity → works |
Two independent vectors
The .NET runtime can enter a child process through two completely independent paths:
| Vector | Mechanism | Fix |
|---|---|---|
| ContentProvider | Auto-generated MonoRuntimeProvider_N calls MonoPackageManager.LoadApplication() in attachInfo(), before Application.onCreate() |
StripMonoRuntimeProviderTask removes these entries from the merged manifest |
| Application class | C# ACW's onCreate() calls n_onCreate() (native JNI), which requires libmonodroid.so |
NativeAppWrapper replaces the C# Application class with pure Java |
Fixing only one vector either crashes the process (Vector 1 fixed,
Vector 2 not) or still loads Mono (Vector 2 fixed, Vector 1 not). Both
must be addressed together, and MauiBootstrapper is the direct
consequence of fixing Vector 2.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
This package has no dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0.1 | 98 | 5/22/2026 |
| 0.0.1-alpha009 | 107 | 5/19/2026 |
| 0.0.1-alpha008 | 90 | 5/14/2026 |