XDev.Android.ProcessIsolation
0.0.1-alpha008
See the version list below for details.
dotnet add package XDev.Android.ProcessIsolation --version 0.0.1-alpha008
NuGet\Install-Package XDev.Android.ProcessIsolation -Version 0.0.1-alpha008
<PackageReference Include="XDev.Android.ProcessIsolation" Version="0.0.1-alpha008"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="XDev.Android.ProcessIsolation" Version="0.0.1-alpha008" />
<PackageReference Include="XDev.Android.ProcessIsolation"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add XDev.Android.ProcessIsolation --version 0.0.1-alpha008
#r "nuget: XDev.Android.ProcessIsolation, 0.0.1-alpha008"
#:package XDev.Android.ProcessIsolation@0.0.1-alpha008
#addin nuget:?package=XDev.Android.ProcessIsolation&version=0.0.1-alpha008&prerelease
#tool nuget:?package=XDev.Android.ProcessIsolation&version=0.0.1-alpha008&prerelease
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
1. AndroidManifest.xml
Point <application android:name> at the shipped wrapper class. The wrapper
ships with a fixed Java package — reference its fully-qualified name as a
literal:
<application android:name="xdev.android.processisolation.NativeAppWrapper"
tools:replace="android:name" ...>
The tools:replace="android:name" is necessary because .NET Android
auto-injects an ACW name (crc...MainApplication) for any C# class with
[Application]. Without tools:replace, the auto-injection wins the merge
and the wrapper never instantiates. If you previously had an [Application]
attribute on a C# class, remove or comment it out — the wrapper is the
Application class now.
${applicationId}elsewhere in the manifest is unchanged.android:nameon<application>is the only place that needs the literal wrapper FQN. Everywhere else —<category android:name="${applicationId}" />for FCM registration,<provider android:authorities="${applicationId}.fileProvider" />, deep-link<data android:host="..." android:path="/${applicationId}" />, etc. — keep using${applicationId}as before. The Android manifest merger still substitutes it with your app package (from<ApplicationId>/package=), which is what intent routing, FCM, ContentProvider authorities, and permissions need. The wrapper's fixed Java package (xdev.android.processisolation) is intentionally separate from your app's package; the two never conflict.
2. MainActivity.OnCreate
If you previously had service registrations inside a
MauiApplication.CreateMauiApp() override (now retired because the wrapper is
your Application class), move them inside the BuildMauiApp delegate —
builder.Services is fully available before MauiProgram.CreateMauiApp runs.
protected override void OnCreate(Bundle? savedInstanceState)
{
// Must be set before Initialize.
MauiBootstrapper.BuildMauiApp = builder =>
{
// Register any services your retired MauiApplication.CreateMauiApp()
// used to register. builder.Services is fully available here.
// builder.Services.AddTransient<IMyService>(_ => new MyService());
return MauiProgram.CreateMauiApp(builder);
};
// Must be called before base.OnCreate — MauiAppCompatActivity relies on
// IPlatformApplication.Current and application.Handler.MauiContext.
MauiBootstrapper.Initialize(Application);
base.OnCreate(savedInstanceState);
}
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 | 103 | 5/22/2026 |
| 0.0.1-alpha009 | 112 | 5/19/2026 |
| 0.0.1-alpha008 | 91 | 5/14/2026 |