-
Notifications
You must be signed in to change notification settings - Fork 1
Design Notes
Why the repository looks the way it does, and the invariants any change must preserve.
The Me.Toolkit.Maui.* package IDs are new. Nothing on nuget.org resolves them, so there are no
consumers and no additive-only guarantee. The API is free to be fixed properly — CrossNfc.Current
can become DI registration, a hand-wired MainActivity.OnNewIntent can become
ConfigureLifecycleEvents, and a dead 2017 dependency can come out of the public surface.
The API baseline still exists, and matters more here than it did in Blazorme, for a different reason: so that changing the surface is a decision rather than an accident. Every change to it is a diff to review in the commit that causes it.
PublicApiSurfaceTests renders every public type, member signature, generic arity, enum numeric
value, const literal and default parameter value into Me.Toolkit.Maui.Tests/PublicApi.approved.txt.
It reads metadata only, through MetadataLoadContext over the built assemblies, rather than
referencing the projects. In Blazorme that was because two libraries targeted net5.0. Here it is
forward-looking: the ported libraries will target MAUI platform frameworks — net10.0-android,
net10.0-ios, net10.0-maccatalyst, net10.0-windows10.0.19041.0 — and a plain net10.0 test
project cannot reference any of them. The machinery was established in Phase 0, while the imported
Xamarin libraries were still plain netstandard2.0 and easy, so that the baseline would survive the
port. It did.
When a change is intentional, review the diff and copy PublicApi.received.txt from the test output
directory over PublicApi.approved.txt, in the same commit. Never weaken the assertion.
The baseline renders each Me.Toolkit.Maui library from its net10.0 slice only. Dumping all five slices
would make the approved file four times longer and mostly repetition, so MultiTargetingTests holds
the platform slices to the neutral one instead: same public surface, every framework. An
Android-only member added by accident — the easy mistake when half a plugin lives behind
#if ANDROID — fails there. A platform-specific member added on purpose means rewriting that test
to name the exception, in the commit that adds it; it must not be weakened into a subset check.
Two things make reading a platform slice work at all:
-
One
MetadataLoadContextper assembly. The slices of a single library are compiled against different worlds, and an AndroidSystem.Runtimeand an iOSSystem.Runtimecannot share a resolver that matches on simple name. A single shared context silently resolves types out of whichever pack was enumerated first. -
Me.Toolkit.Maui.ReferencePaths.txt, written beside every assembly byDirectory.Build.targets. A library build does not copy its dependencies, soMono.Android,Microsoft.iOSand the rest are nowhere nearbin/. Guessing at the workload's reference-assembly folders would be a proxy; theReferencePathitem is what the compiler was actually handed.
That this works is verified rather than assumed: a temporary type exposing
Android.Nfc.NfcAdapter, CoreNFC.NFCNdefReaderSession and
Windows.Devices.SmartCards.SmartCardReader rendered correctly in each slice, from a net10.0 test
project that can reference none of them.
PathAssemblyResolver matches on version as well as simple name, which is more precision than a
metadata dump needs and more than the toolchain reliably offers — a reference assembly and the
runtime assembly it stands in for need not agree on version, and rendering a type name does not
depend on telling them apart.
It was originally replaced for a blunter reason: legacy/WebHostPatch dropped a fork of
Microsoft.Extensions.Primitives stamped 5.9.0.0 into every output directory that referenced it,
shadowing the real assembly, so the version the metadata asked for was nowhere on disk and the
baseline threw FileNotFoundException. That fork is gone; the resolver stays on its own merits.
A slice resolves only against its own Me.Toolkit.Maui.ReferencePaths.txt, with nothing from the host
runtime mixed in, so an Android slice takes System.Runtime from the Android ref pack rather than
from whatever runtime the tests happen to be running on.
All four are the same shape, and the shape was settled by building rather than by reading:
-
Microsoft.Maui.Core, notMicrosoft.Maui.Controls. Core carriesMauiAppBuilder, theLifecycleEventsbuilders andMicrosoft.Maui.ApplicationModel.Microsoft.Maui.Essentialshas none of the first two. An NFC plugin has no business depending on Controls, and now does not. -
Me.Toolkit.Maui.WebHostPatchtakes ASP.NET Core as netstandard2.0 packages, never theMicrosoft.AspNetCore.Appframework reference — that framework has no mobile runtime pack. -
net10.0alongside the four platform frameworks. It is what a non-platform project resolves, and forMe.Toolkit.Maui.Nfcit is where "this platform has no implementation" lives — the roleXamarinme.Nfcgavenetstandard2.0. -
AndroidGenerateResourceDesigner=false. Android otherwise generates a publicResourceclass into every library, resources or not, and it lands in the package's public surface.
The target framework list lives once, as $(MeToolkitMauiTargetFrameworks) in Directory.Build.props,
which also turns on documentation generation for the four Me.Toolkit.Maui.* projects — so CS1591 requires
every public member to be documented.
The first question, per library, was whether it should exist at all. Verified rather than assumed —
a net10.0-android probe project was built to check the MAUI API, and the imported Xamarin projects
were built to check what still compiled.
XamarinHostBuilder exposed Configuration, Services, HostEnvironment and Logging, and a Build().
MauiAppBuilder exposes the same, member for member. XamarinHostConfiguration is a copy of
Blazor's WebAssemblyHostConfiguration; ConfigurationManager is the maintained equivalent. And
MAUI registers an IHostEnvironment of its own — which Phase 0 could not confirm and Phase 3
did.
So the only gap is that MAUI's environment always reports Production. That is what
Me.Toolkit.Maui.Hosting fills, and it is the whole library.
It wraps rather than assigns, because MauiHostEnvironment.EnvironmentName throws
NotImplementedException from its setter. The interface declares the property as settable and the
type reports CanWrite, so nothing short of calling it reveals otherwise — the probe that found the
registration got this wrong, and a test caught it. Everything but the name is delegated, so
ApplicationName and ContentRootPath still come from the platform, including their failure modes.
XamarinHost did not come across at all: it implemented IHost and threw
NotImplementedException from both StartAsync and StopAsync.
Two ways in — AddAppPackageJson for a MauiAsset, AddEmbeddedResourceJson for the embedded
resource Xamarinme used — both layering appsettings.{environment}.json over the base file.
The parser is Microsoft's, referenced not vendored. This is the opposite call from
Me.Toolkit.Maui.Nfc's, and deliberately: NdefLibrary was a dead 2017 package sitting in the public API,
while Microsoft.Extensions.Configuration.Json is a live first-party component. Reimplementing it
to keep a zero-dependency badge would have been the same vendoring mistake Xamarinme made, in newer
clothes. MAUI does not reference it, which is the one thing this package supplies that an app could
not already do in a line.
The rendering differences are narrower than they look, and were measured rather than guessed. Both
parsers render a JSON true as "True", through bool.ToString() and JsonElement.ToString()
respectively — the obvious assumption that Microsoft emits the raw token is wrong, and the test that
assumed it failed. The one real difference is a JSON null: "" before, absent now.
The options object is gone, and with it three ways to turn a mistake into a NullReferenceException
out of Build() — or, for a wrong prefix, into an empty configuration and no error at all.
The app-package path has no coverage. MAUI's FileSystem on the net10.0 slice is the
reference-assembly stub and throws, so what is untested is the four lines that open the asset; both
paths funnel into the same layering code.
The two forks existed for two Mono-era problems, and both causes are gone:
-
Microsoft.Net.Http.Headers2.2.0 callsInplaceStringBuilder, whichMicrosoft.Extensions.Primitives5.0 deleted. The vendored fork restores the type and stamps itself 5.9.0.0 so it wins at bind time. This exists only because of the ASP.NET Core 2.2.0 pin. -
Console.CancelKeyPressthrew on Mono, soConsoleLifetimeandWebHostExtensions.RunAsyncwere forked to route around it.
The approach is unchanged, because it is the only one that works. ASP.NET Core comes from
netstandard2.0 packages — the 2.3.x servicing line — which are copied into the app like any
assembly. That is precisely why Xamarinme.WebHostPatch ran on Xamarin.
The modern-looking alternative does not work, and finding that out cost a phase. The
Microsoft.AspNetCore.App shared framework has no runtime pack for android, ios or maccatalyst —
those RIDs 404 on nuget.org. A framework reference therefore compiles a library against the
reference assemblies quite happily and then fails the consuming app with NETSDK1082. Phase 3
shipped exactly that mistake, because the Phase 1 probe that "verified" it built a library and never
an app. A reference assembly is not a deployment.
The two forks are still gone, but for narrower reasons than "the causes evaporated":
-
InplaceStringBuilderis fixed upstream:Microsoft.Net.Http.Headers2.3.11 no longer references it. Verified by grepping both assemblies, after first checking the same grep finds it in 2.2.0 — the first attempt usedstrings, which is not installed here, and silently reported zero for everything. -
CancelKeyPressis avoided by construction. The call is still there inMicrosoft.AspNetCore.Hosting2.3.11, but it lives inRunAsync; this starts and stops the host explicitly, which never enters that path and is what an app wants anyway. The generic host'sConsoleLifetimeis not involved either, since this is the web host.
What is left is the part that was never the patch: starting and stopping the server, and knowing what address to show the user.
The name is kept for continuity with the 30,366 downloads of Xamarinme.WebHostPatch, and is now a
slight misnomer. That is a deliberate trade.
The old package is not merely obsolete, it is unsafe. Its 2.2.0 pins carry a critical advisory (GHSA-5rrx-jjjq-q2r5, Kestrel) and a moderate one (GHSA-prrf-397v-83xh, IIS), and its forked Primitives shadows the real assembly for anything sharing an output directory — which is why this repository needs two test projects.
MAUI has no NFC support, so this is the library the port was actually for. It was never published, so there was not even an old version to stay compatible with — which is why the API is smaller than what it replaced rather than a translation of it.
INfc is the surface, resolved from DI after builder.UseMeToolkitMauiNfc(). Every implementation is
internal, which is what allows MultiTargetingTests to hold all five slices to one public API.
Gone: CrossNfc.Current (a 2019 static locator), the per-platform public Nfc classes, the unused
NfcTagStatus, and the Nfc.OnNewIntent(intent) call consumers had to add to their own
MainActivity — that hook is now wired by the library through ConfigureLifecycleEvents.
The NDEF types are ours. NdefLibrary 4.1.0 was verified to restore, compile and run on
.NET 10, so dropping it was a choice rather than a forced move: it is a 2017 package with a
netstandard1.4 asset only, and it was in the public API, since ReadNdefAsync returned its
NdefMessage. Me.Toolkit.Maui.Nfc has no third-party dependencies at all.
The codec is pinned against NdefLibrary's own output. Every byte vector in NdefTests was captured
by running NdefLibrary 4.1.0 and recording what it emitted, because the port's real risk is a codec
that disagrees with the old one and corrupts tags silently. Do not regenerate those vectors from
the code under test. Chunked records are refused rather than mis-parsed.
Android and iOS only. Mac Catalyst has no NFC hardware API — CoreNFC is iOS-only — and Windows
would mean the 678-line PC/SC layer and three PCSC packages, for an external USB reader rather
than phone NFC. Those two share the platform-neutral implementation, which throws
PlatformNotSupportedException, so an app targeting everything needs no conditional registration.
Platform sources are selected by explicit Compile Include conditions. The Platforms/ folder
convention is an app thing; in a class library those files compile into every target framework,
which was checked rather than assumed.
Neither platform implementation has behavioural coverage. A net10.0 test project resolves the
net10.0 slice, which is the one with no implementation, so nothing exercises AndroidNfc or
IosNfc. They are held by the API baseline, by MultiTargetingTests and by source pins — which is
a real guard on their shape and none at all on their behaviour.
That includes the two most valuable fixes the port made: the PendingIntent flags, without which
EnableSessionAsync throws on every Android 12 or later device, and the CoreNFC callbacks, which
used to throw on a dispatch queue where nothing could observe it and left callers awaiting forever.
LIMITATION_The_Android_and_iOS_implementations_have_no_behavioural_coverage asserts the gap so it
fails if it ever closes. Closing it needs a device-test harness, which was deliberately not taken
on.
-
Microsoft.Extensions.FileProviders.Xamarin— an emptyClass1, referenced by nothing, not even byWebHostPatch. Never imported into this repository. -
Xamarin.Essentials.FileSystem.Extensions— one method, in the solution, referenced by nothing, never published, and broken by construction: it declarespublic partial class FileSysteminnamespace Xamarin.Essentials, hijacking a Microsoft type name, and callsAssembly.GetExecutingAssembly(), which returns the library's assembly and so can never find the caller's embedded resource.FileSystem.OpenAppPackageFileAsyncreplaces it. Never imported. -
Microsoft.Extensions.Primitives.Patch— imported only becauselegacy/WebHostPatchwould not build without it, and deleted withlegacy/in Phase 4.
- The .NET 10 SDK's
dotnet new slnproducesMe.Toolkit.Maui.slnx, not a classic.sln. The API baseline finds the repository root by that exact file name. - The MAUI workloads present here are
android,ios,maccatalystandmaui-windows. Anet10.0-androidMAUI app builds clean in about 20 seconds. - The iOS and Mac Catalyst library slices compile on Windows. Only building and signing an app for them needs a Mac, so CI on Linux will build two slices per library rather than five — but a macOS runner is still needed to build the demo app for those platforms later.
-
UseMaui/UseMauiCore/UseMauiEssentialsno longer add package references implicitly since .NET 8. MSBuild says so with MA002. - Android drops a public
Resourcedesigner class into every library unlessAndroidGenerateResourceDesigneris turned off.