Skip to content

[BlazorWebView] Public API for external BlazorWebView backends - #37858

Open
Redth wants to merge 3 commits into
net11.0from
redth-blazorwebview-external-backend-apis
Open

[BlazorWebView] Public API for external BlazorWebView backends#37858
Redth wants to merge 3 commits into
net11.0from
redth-blazorwebview-external-backend-apis

Conversation

@Redth

@Redth Redth commented Aug 26, 2026

Copy link
Copy Markdown
Member

Note

Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!

Description of Change

BlazorWebView already supports third-party platform backends: IBlazorWebViewHandler and IMauiBlazorWebViewBuilder.UsePlatformHandler are public, and BlazorWebView routes through the interface rather than the concrete handler. But several parts of what a handler actually has to do were still internal, so an out-of-repo backend had to drop functionality or copy MAUI source.

These gaps were found by building a real external backend against Microsoft.AspNetCore.Components.WebView.Maui 11.0.0-preview.7, and are reproduced in this PR by an in-repo test assembly that is deliberately not granted InternalsVisibleTo.

1. Platform-neutral native web view on BlazorWebViewInitializedEventArgs

BlazorWebViewInitializedEventArgs.WebView is declared only under #if WINDOWS / ANDROID / IOS || MACCATALYST / TIZEN. On any other target framework the type has no members at all, so a third-party handler cannot populate BlazorWebViewInitialized with anything meaningful.

New, gated on WEBVIEW2_MAUI so it is scoped to the MAUI package:

public BlazorWebViewInitializedEventArgs(object platformWebView);
public object? PlatformWebView { get; }

The value is read-only and write-once: only the handler raising the event can supply it, either through the new constructor or — for the built-in handlers — through the existing typed WebView property, whose setter is still internal and now throws if the value has already been set. An event subscriber cannot change what later subscribers observe.

On target frameworks where the typed WebView exists it reads through the same backing field:

public WKWebView WebView
{
    get => _platformWebView as WKWebView;
    internal set => SetPlatformWebView(value);
}

so e.WebView returns exactly what it did before for Android/iOS/MacCatalyst/Windows, and a mismatched value returns null rather than throwing. The WPF and WinForms packages are unchanged — they keep only their existing typed WebView property, and their PublicAPI.Unshipped.txt files are back at baseline.

2. Public RootComponent lifecycle

RootComponent.AddToWebViewManagerAsync / RemoveFromWebViewManagerAsync were internal, so every third-party backend had to re-derive the "Selector is required" / "ComponentType is required" validation and the add/remove ordering. Both are now public with docs and null-argument checks. The validation logic, messages and ordering are unchanged, and the parameter is named webViewManager on both.

3. Static content hot reload seam

StaticContentHotReloadManager stays internal — it is a [MetadataUpdateHandler] target with mutable static state, and its ref-mutating response method is not a shape we want third-party backends binding to. Instead, a public seam exposes a query and an observable attach:

public static class BlazorWebViewStaticContentHotReload
{
    public static Task? TryAttachToWebViewManager(WebViewManager webViewManager);

    public static bool TryGetUpdatedStaticContent(
        string contentRootRelativePath, string requestAbsoluteUri,
        out Stream? content, out string? contentType);
}
  • TryGetUpdatedStaticContent only reports content. The caller owns its own status code, headers and disposal, and gets a fresh stream per call. The ref-mutating helper is retained internally for the built-in managers and is implemented on top of this public method.
  • TryAttachToWebViewManager returns null when hot reload is unsupported and nothing was attached, otherwise the Task for the notifier registration, so callers can observe or await it. Attaching is idempotent per WebViewManager instance — a repeat call returns the first task instead of failing on the notifier's fixed body::after selector.
  • TryDetachFromWebViewManager completes the lifecycle. It returns null when nothing was attached, otherwise the Task for removing the notifier, sequenced after any in-flight attach so it cannot race into "there is no root component with selector body::after". It is idempotent and clears the weak attach entry, so a handler that is disconnected and later reconnected can attach again — previously a second attach silently replayed the first task and the notifier was never re-registered.
  • The in-box handlers use the public seam directly, at 12 call sites — attach, detach and content lookup on each of Android, iOS, Tizen and Windows. The MAUI-side ref-mutating convenience wrapper was deleted; the managers now call TryGetUpdatedStaticContent and apply the result to their own response state, so the shipping code path is the public API rather than a parallel one. The remaining internal ref-based helper is reachable only from the WEBVIEW2_WINFORMS || WEBVIEW2_WPF branch of WebView2WebViewManager, which cannot reference the MAUI-only type.

4. Public dispatcher adapter

MauiDispatcher, the IDispatcher → Blazor Dispatcher adapter the built-in handlers use, was internal, so external backends copied it. It is now public sealed with docs and an argument check:

var dispatcher = new MauiDispatcher(Services!.GetRequiredService<IDispatcher>());

Behavior preserved

  • Android / iOS / MacCatalyst / Windows / WPF / WinForms handler code is behaviorally unchanged; WebView = _webview in the existing object initializers still works.
  • The shipped ~...BlazorWebViewInitializedEventArgs.WebView.get PublicAPI entries are unchanged — the property shape (public get, internal set) is the same.
  • Root component validation, messages and ordering are unchanged.
  • Hot reload semantics, MetadataUpdateHandler registration and the MetadataUpdater.IsSupported gate are unchanged; only the shape of the API around them is new, plus attach idempotence which previously would have thrown.
  • No new trimming/AOT annotations were needed; trim/AOT/single-file analyzers are on and the build is warning-free.

Intentionally still internal

The static content response cache and its policy helpers (StaticContentResponseCache, StaticContentResponseCachePolicy, StaticContentCacheControl, QueryStringHelper) stay internal so their storage shape, eviction, entry-size limits and Cache-Control/Pragma parsing remain free to change. This is now documented, along with the fact that IBlazorWebView.StaticContentCacheControlProvider is public for apps to influence the emitted header.

Tests

New src/BlazorWebView/tests/MauiBlazorWebView.ExternalHandler.UnitTests (added to Microsoft.Maui.sln, -dev.sln, -vscode.sln, both .slnf files and eng/helix.proj).

It stands in for a third-party backend package: no InternalsVisibleTo grant, and a test that fails if one is ever added. It contains a fake external IBlazorWebViewHandler, a fake WebViewManager built on the public MauiDispatcher, and a fake IDispatcher.

43 tests covering:

  • PlatformWebView defaults, constructor supply, null rejection, that the property has no setter, that it flows through the real BlazorWebViewInitialized event with the correct sender, and that two subscribers observe the same instance.
  • RootComponent add/remove through the public API: registration, parameters, missing Selector, missing ComponentType, duplicate selector, removing an unregistered selector, null manager, and add-in-collection-order from the handler.
  • The hot reload seam: attach reporting whether it attached, attach idempotence for one manager, independence across managers, attach during handler startup, detach removing the notifier, detach returning null when nothing was attached, detach idempotence, re-attach after detach, and teardown through an external handler, plus unknown content, serving the _framework/static-content-hot-reload.js payload with text/javascript, a fresh independently-disposable stream per call, and argument validation.
  • MauiDispatcher public construction, null rejection, and that it really dispatches through the supplied IDispatcher.
  • Reflection guards that no public ref-mutating API exists on the seam and that the root component parameter is named webViewManager.

Static content hot reload only activates when the runtime is started with DOTNET_MODIFIABLE_ASSEMBLIES=debug. eng/helix.proj already sets that for every xUnit work item, so the enabled branch is the one CI runs; the suite was verified green locally in both modes.

API surface was validated with -p:PublicApiType=Validate on net11.0 and net11.0-android37.0 (local Debug builds default to Generate, which does not validate), and the API delta is byte-identical across all six TFM folders.

Also adds docs/design/BlazorWebViewExternalBackends.md describing the full external-backend contract.

Issues Fixed

None filed; found while building an external BlazorWebView backend against the .NET 11 packages.

Third-party BlazorWebView handlers can already be registered through the public
IBlazorWebViewHandler + UsePlatformHandler contract, but three pieces of the handler
contract were still internal, forcing external backends to either skip functionality
or duplicate MAUI source.

Adds the smallest additive, backward-compatible seams for each:

- BlazorWebViewInitializedEventArgs.NativeWebView: a platform-neutral object property
  so a handler on a target framework without a built-in MAUI backend can surface its
  native control. On target frameworks where the strongly typed WebView property
  exists, both are backed by the same value (WebView now reads through NativeWebView
  with an 'as' conversion, so it returns null rather than throwing on a mismatch).
  Existing app code reading e.WebView is unaffected.

- RootComponent.AddToWebViewManagerAsync / RemoveFromWebViewManagerAsync are now
  public, so external handlers reuse MAUI's validation and ordering instead of
  reimplementing them. Both now null-check the manager argument.

- BlazorWebViewStaticContentHotReload: a public seam over the internal
  StaticContentHotReloadManager exposing AttachToWebViewManagerIfEnabled and
  TryReplaceResponseContent, so external handlers can participate in MAUI Blazor
  static content hot reload with identical behavior.

Adds src/BlazorWebView/tests/MauiBlazorWebView.ExternalHandler.UnitTests, an assembly
that is deliberately not granted InternalsVisibleTo. It hosts a fake external handler
and WebViewManager and proves all three seams are usable without privileged access or
copied source, including a test that fails if InternalsVisibleTo is ever added.

Also documents the contract in docs/design/BlazorWebViewExternalBackends.md.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings August 26, 2026 21:39
@Redth
Redth temporarily deployed to copilot-pat-pool August 26, 2026 21:39 — with GitHub Actions Inactive
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 37858

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 37858"

@Redth
Redth temporarily deployed to copilot-pat-pool August 26, 2026 21:39 — with GitHub Actions Inactive
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
There may be pipelines that require an authorized user to comment /azp run to run.

@Redth
Redth temporarily deployed to copilot-pat-pool August 26, 2026 21:40 — with GitHub Actions Inactive
@Redth
Redth temporarily deployed to copilot-pat-pool August 26, 2026 21:42 — with GitHub Actions Inactive
@Redth
Redth temporarily deployed to copilot-pat-pool August 26, 2026 21:43 — with GitHub Actions Inactive
@github-actions github-actions Bot added the area-blazor Blazor Hybrid / Desktop, BlazorWebView label Aug 26, 2026
@Redth
Redth temporarily deployed to copilot-pat-pool August 26, 2026 21:44 — with GitHub Actions Inactive

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds small, additive public API seams in the MAUI BlazorWebView stack to enable fully out-of-repo (third-party) platform backends without needing InternalsVisibleTo or copied MAUI source, and backs it with an “external handler” unit test project plus design documentation.

Changes:

  • Add BlazorWebViewInitializedEventArgs.NativeWebView (shared across MAUI/WPF/WinForms) and wire existing typed WebView properties to the same backing value.
  • Make RootComponent add/remove lifecycle methods public and introduce a public static-content hot reload seam (BlazorWebViewStaticContentHotReload).
  • Add MauiBlazorWebView.ExternalHandler.UnitTests, wire it into solutions + Helix, and document the external-backend contract.

Reviewed changes

Copilot reviewed 26 out of 26 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/BlazorWebView/tests/MauiBlazorWebView.ExternalHandler.UnitTests/TestRootComponents.cs Adds minimal fake components used by external-backend tests.
src/BlazorWebView/tests/MauiBlazorWebView.ExternalHandler.UnitTests/MauiBlazorWebView.ExternalHandler.UnitTests.csproj New unit test project that intentionally has no internals access.
src/BlazorWebView/tests/MauiBlazorWebView.ExternalHandler.UnitTests/FakeExternalWebViewManager.cs Fake WebViewManager implementation for external-backend simulations.
src/BlazorWebView/tests/MauiBlazorWebView.ExternalHandler.UnitTests/FakeExternalBlazorWebViewHandler.cs Fake external IBlazorWebViewHandler exercising the public seams.
src/BlazorWebView/tests/MauiBlazorWebView.ExternalHandler.UnitTests/ExternalStaticContentHotReloadTests.cs Tests the new hot reload seam behavior and argument validation.
src/BlazorWebView/tests/MauiBlazorWebView.ExternalHandler.UnitTests/ExternalRootComponentTests.cs Tests public RootComponent lifecycle methods against a manager.
src/BlazorWebView/tests/MauiBlazorWebView.ExternalHandler.UnitTests/ExternalNativeWebViewEventArgsTests.cs Tests NativeWebView behavior and event wiring from an external handler.
src/BlazorWebView/tests/MauiBlazorWebView.ExternalHandler.UnitTests/ExternalHandlerContractTests.cs Guards the “no InternalsVisibleTo” premise and asserts key APIs are public.
src/BlazorWebView/src/Wpf/PublicAPI.Unshipped.txt Public API baseline update for NativeWebView.
src/BlazorWebView/src/WindowsForms/PublicAPI.Unshipped.txt Public API baseline update for NativeWebView.
src/BlazorWebView/src/SharedSource/BlazorWebViewInitializedEventArgs.cs Adds NativeWebView and backs typed WebView properties with it.
src/BlazorWebView/src/Maui/RootComponent.cs Makes add/remove lifecycle public and adds XML docs + null checks.
src/BlazorWebView/src/Maui/PublicAPI/net/PublicAPI.Unshipped.txt Public API baseline updates for new seams.
src/BlazorWebView/src/Maui/PublicAPI/net-windows/PublicAPI.Unshipped.txt Public API baseline updates for new seams.
src/BlazorWebView/src/Maui/PublicAPI/net-tizen/PublicAPI.Unshipped.txt Public API baseline updates for new seams.
src/BlazorWebView/src/Maui/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt Public API baseline updates for new seams.
src/BlazorWebView/src/Maui/PublicAPI/net-ios/PublicAPI.Unshipped.txt Public API baseline updates for new seams.
src/BlazorWebView/src/Maui/PublicAPI/net-android/PublicAPI.Unshipped.txt Public API baseline updates for new seams.
src/BlazorWebView/src/Maui/BlazorWebViewStaticContentHotReload.cs Introduces the public static-content hot reload seam.
Microsoft.Maui.sln Adds the new ExternalHandler unit test project to the main solution.
Microsoft.Maui-windows.slnf Adds the new test project to the Windows solution filter.
Microsoft.Maui-vscode.sln Adds the new ExternalHandler unit test project to the VSCode solution.
Microsoft.Maui-mac.slnf Adds the new test project to the Mac solution filter.
Microsoft.Maui-dev.sln Adds the new ExternalHandler unit test project to the dev solution.
eng/helix.proj Includes the new unit test project in Helix xUnit runs.
docs/design/BlazorWebViewExternalBackends.md Documents the supported external-backend contract and required seams.
Suppressed comments (1)

src/BlazorWebView/src/Maui/RootComponent.cs:79

  • Now that this is public API, the parameter name webviewManager (lowercase ‘V’) is inconsistent with AddToWebViewManagerAsync(WebViewManager webViewManager) and will show up in IntelliSense. Consider renaming it to webViewManager before shipping, and update the corresponding PublicAPI.*.txt baselines to match.
		public Task RemoveFromWebViewManagerAsync(WebViewManager webviewManager)

Comment on lines +1 to +3
using System;
using System.Collections.Generic;
using System.IO;
{
ArgumentNullException.ThrowIfNull(webViewManager);

// As a characteristic of XAML,we can't rely on non-default constructors. So we have to
@kubaflo

This comment has been minimized.

@github-actions github-actions Bot added the s/agent-review-in-progress AI review is currently running for this PR label Aug 26, 2026
Reworks the public surface added in the previous commit per design review.

1. Hot reload content is now a query, not a ref-mutation. The public API is
   TryGetUpdatedStaticContent(contentRootRelativePath, requestAbsoluteUri,
   out Stream? content, out string? contentType); the caller owns the response
   status, headers and disposal, and gets a fresh stream per call. The
   ref-mutating shape stays internal for the built-in managers.

2. The platform-neutral native view is now write-once and read-only. NativeWebView
   (public get/set) is replaced by PlatformWebView (public get only), supplied
   either through a new public BlazorWebViewInitializedEventArgs(object) ctor or,
   for the built-in handlers, through the existing internal typed WebView setter,
   which now throws if the value has already been set. An event subscriber can no
   longer change what later subscribers observe.

3. The property is scoped to the MAUI package. It is gated on WEBVIEW2_MAUI, so
   the WPF and WinForms packages keep only their existing typed WebView property
   and their PublicAPI files return to baseline.

4. Attach is observable and idempotent. TryAttachToWebViewManager returns null
   when hot reload is unsupported, otherwise the Task for the notifier
   registration. Repeat calls for the same manager return the first task instead
   of failing on the fixed root component selector. All four built-in MAUI
   handlers and their web view managers now route through the public seam, so the
   external contract is dogfooded.

5. The IDispatcher-to-Blazor-Dispatcher adapter, MauiDispatcher, is now public and
   argument-checked, so external backends stop copying it. Docs state explicitly
   that the static content response cache and its policy helpers stay internal
   and why.

6. RootComponent.RemoveFromWebViewManagerAsync's parameter is renamed
   webviewManager -> webViewManager to match AddToWebViewManagerAsync.

External test assembly grows to 37 tests, still with no InternalsVisibleTo,
covering read-only/write-once semantics, all-subscribers-see-the-same-instance,
attach idempotence and per-manager independence, fresh-stream-per-call and caller
ownership, public MauiDispatcher construction and dispatch, and reflection guards
that no public ref-mutating API exists and that the lifecycle parameter is named
correctly. Verified green with hot reload both disabled and enabled
(DOTNET_MODIFIABLE_ASSEMBLIES=debug).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 26, 2026 22:41
@MauiBot

MauiBot commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

AI Review Summary

@Redth — new AI review results are available based on commit 63a0376.

Gate Inconclusive Confidence Unknown Platform Windows


🗂️ Review Sessions — click to expand

[!WARNING]
This run reviewed commit 63a0376, but the PR advanced to 1d8d778 while it was running. These results are informational; re-run /review for the current head.


🚦 Gate — Test Before & After Fix

Gate Result: ⚠️ INCONCLUSIVE

Platform: WINDOWS

⚠️ The gate could not conclusively verify the fix on this run: it hit an environment/infrastructure error while building or running the tests (exit code 3 = INCONCLUSIVE), so no reliable pass/fail was produced. This is not a problem with your PR — comment /review to retry on a fresh agent. The diagnostics below show what was captured before it stopped.

Exit code: 3

Artifacts written before exit:

  • verification-log.txt (9.1 KB)
Gate output log (last 60 lines)
[2026-08-26 22:14:43]   ✓ src/BlazorWebView/src/SharedSource/BlazorWebViewInitializedEventArgs.cs (exists at merge-base - will revert)
[2026-08-26 22:14:43]   ✓ src/BlazorWebView/src/WindowsForms/PublicAPI.Unshipped.txt (exists at merge-base - will revert)
[2026-08-26 22:14:43]   ✓ src/BlazorWebView/src/Wpf/PublicAPI.Unshipped.txt (exists at merge-base - will revert)
[2026-08-26 22:14:43] 
[2026-08-26 22:14:43] Checking for uncommitted changes on revertable files...
[2026-08-26 22:14:44]   ✓ All revertable fix files are committed
[2026-08-26 22:14:44] 
[2026-08-26 22:14:44] ==========================================
[2026-08-26 22:14:44] STEP 1: Reverting fix files to merge-base (bedd1b18)
[2026-08-26 22:14:44] ==========================================
[2026-08-26 22:14:44]   Reverting: Microsoft.Maui-dev.sln
[2026-08-26 22:14:44]   Reverting: Microsoft.Maui-mac.slnf
[2026-08-26 22:14:44]   Reverting: Microsoft.Maui-vscode.sln
[2026-08-26 22:14:44]   Reverting: Microsoft.Maui-windows.slnf
[2026-08-26 22:14:44]   Reverting: Microsoft.Maui.sln
[2026-08-26 22:14:44]   Reverting: eng/helix.proj
[2026-08-26 22:14:44]   Reverting: src/BlazorWebView/src/Maui/PublicAPI/net-android/PublicAPI.Unshipped.txt
[2026-08-26 22:14:44]   Reverting: src/BlazorWebView/src/Maui/PublicAPI/net-ios/PublicAPI.Unshipped.txt
[2026-08-26 22:14:45]   Reverting: src/BlazorWebView/src/Maui/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
[2026-08-26 22:14:45]   Reverting: src/BlazorWebView/src/Maui/PublicAPI/net-tizen/PublicAPI.Unshipped.txt
[2026-08-26 22:14:45]   Reverting: src/BlazorWebView/src/Maui/PublicAPI/net-windows/PublicAPI.Unshipped.txt
[2026-08-26 22:14:45]   Reverting: src/BlazorWebView/src/Maui/PublicAPI/net/PublicAPI.Unshipped.txt
[2026-08-26 22:14:45]   Reverting: src/BlazorWebView/src/Maui/RootComponent.cs
[2026-08-26 22:14:45]   Reverting: src/BlazorWebView/src/SharedSource/BlazorWebViewInitializedEventArgs.cs
[2026-08-26 22:14:45]   Reverting: src/BlazorWebView/src/WindowsForms/PublicAPI.Unshipped.txt
[2026-08-26 22:14:45]   Reverting: src/BlazorWebView/src/Wpf/PublicAPI.Unshipped.txt
[2026-08-26 22:14:45]   ✓ 16 fix file(s) reverted to merge-base state
[2026-08-26 22:14:45] 
[2026-08-26 22:14:45]   Removing 1 PR-added file(s) so the baseline matches the pre-fix tree:
[2026-08-26 22:14:45]     Removing (new in PR): src/BlazorWebView/src/Maui/BlazorWebViewStaticContentHotReload.cs
[2026-08-26 22:14:45]   ✓ 1 PR-added file(s) removed for the baseline
╔═══════════════════════════════════════════════════════════╗
║  STEP 2: Running tests WITHOUT fix (expect FAIL)          ║
╚═══════════════════════════════════════════════════════════╝
[2026-08-26 22:14:45] 
[2026-08-26 22:14:45] STEP 2: Running tests WITHOUT fix (should FAIL)
##[group]🔴 WITHOUT FIX 1/4: 🧪 ExternalHandlerContractTests (filter: ExternalHandlerContractTests)
❌ Could not determine unit test project to run.
   Detected project: 
   Path: 
[2026-08-26 22:14:45] 
[2026-08-26 22:14:45] ⚠️  Verification ended inside the baseline mutation window — restoring the with-fix tree from HEAD
[2026-08-26 22:14:45]   Restoring: Microsoft.Maui-dev.sln
[2026-08-26 22:14:45]   Restoring: Microsoft.Maui-mac.slnf
[2026-08-26 22:14:45]   Restoring: Microsoft.Maui-vscode.sln
[2026-08-26 22:14:45]   Restoring: Microsoft.Maui-windows.slnf
[2026-08-26 22:14:45]   Restoring: Microsoft.Maui.sln
[2026-08-26 22:14:45]   Restoring: eng/helix.proj
[2026-08-26 22:14:45]   Restoring: src/BlazorWebView/src/Maui/PublicAPI/net-android/PublicAPI.Unshipped.txt
[2026-08-26 22:14:45]   Restoring: src/BlazorWebView/src/Maui/PublicAPI/net-ios/PublicAPI.Unshipped.txt
[2026-08-26 22:14:45]   Restoring: src/BlazorWebView/src/Maui/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
[2026-08-26 22:14:46]   Restoring: src/BlazorWebView/src/Maui/PublicAPI/net-tizen/PublicAPI.Unshipped.txt
[2026-08-26 22:14:46]   Restoring: src/BlazorWebView/src/Maui/PublicAPI/net-windows/PublicAPI.Unshipped.txt
[2026-08-26 22:14:46]   Restoring: src/BlazorWebView/src/Maui/PublicAPI/net/PublicAPI.Unshipped.txt
[2026-08-26 22:14:46]   Restoring: src/BlazorWebView/src/Maui/RootComponent.cs
[2026-08-26 22:14:46]   Restoring: src/BlazorWebView/src/SharedSource/BlazorWebViewInitializedEventArgs.cs
[2026-08-26 22:14:46]   Restoring: src/BlazorWebView/src/WindowsForms/PublicAPI.Unshipped.txt
[2026-08-26 22:14:46]   Restoring: src/BlazorWebView/src/Wpf/PublicAPI.Unshipped.txt
[2026-08-26 22:14:46]   Restoring (new in PR): src/BlazorWebView/src/Maui/BlazorWebViewStaticContentHotReload.cs
[2026-08-26 22:14:46]   ✓ Worktree/index restored to HEAD

📋 Pre-Flight — Context & Validation

PR #37858 Pre-Flight

PR context

  • Title: [BlazorWebView] Public API for external BlazorWebView backends
  • Base / head: net11.0 / redth-blazorwebview-external-backend-apis
  • Materialized review commit: 056d74fdefde20ba159d2b4b8d32471615cb215a
  • Scope: 26 files, 975 additions, 7 deletions.
  • Problem: An out-of-repository IBlazorWebViewHandler cannot fully implement the built-in handler contract because the native view event payload is platform-conditional, the RootComponent manager lifecycle methods are internal, and static-content hot reload is internal.

Existing PR approach

The diff adds three additive public seams:

  1. BlazorWebViewInitializedEventArgs.NativeWebView is a public object? property. Existing platform-typed WebView properties now share that backing value and use safe as casts.
  2. RootComponent.AddToWebViewManagerAsync and RemoveFromWebViewManagerAsync become public, gain API documentation, and validate a null manager.
  3. A new BlazorWebViewStaticContentHotReload public facade forwards attach and response-replacement operations to the internal StaticContentHotReloadManager.

The PR also updates the PublicAPI files for all relevant target frameworks, adds design documentation and solution/Helix wiring, and adds a deliberately non-friend external-handler unit-test assembly.

Direct diff observations

  • Existing built-in Android, iOS/MacCatalyst, Tizen, and Windows handlers retain their current call sites.
  • The new native-view API changes the typed property implementation but preserves its public-get/internal-set shape.
  • Root-component validation and manager call ordering remain in RootComponent.
  • The hot-reload facade duplicates argument-null validation before forwarding to the existing internal implementation.
  • The PR introduces a new production file, src/BlazorWebView/src/Maui/BlazorWebViewStaticContentHotReload.cs; try-fix baseline safety rules may therefore block alternative edits if .github/.baseline-state.json reports it in NewFiles.

Targeted test surface

Primary test project:

dotnet test src\BlazorWebView\tests\MauiBlazorWebView.ExternalHandler.UnitTests\MauiBlazorWebView.ExternalHandler.UnitTests.csproj

This project contains 28 focused tests across:

  • ExternalHandlerContractTests
  • ExternalNativeWebViewEventArgsTests
  • ExternalRootComponentTests
  • ExternalStaticContentHotReloadTests

No additional mandatory regression-test command was enumerated in the STEP 5a request. Do not run a broader test suite.

Gate status

The existing gate is INCONCLUSIVE, not a fix failure. It stopped before running tests because the detector could not resolve the newly added unit-test project after establishing the without-fix baseline. Do not rerun gate verification and do not modify gate/content.md.

Try-fix constraints

  • Platform: Windows.
  • Each attempt must use only the targeted project above.
  • One implementation/test pass is allowed, plus at most one focused correction/retest.
  • Each attempt must restore exclusively with pwsh .github/scripts/EstablishBrokenBaseline.ps1 -Restore.
  • Candidate 2 must avoid Candidate 1's mechanism and consume its recorded result without reopening or rerunning it.

🔬 Code Review — Deep Analysis

Expert PR Evaluation

Independent assessment

The submitted change adds the missing public surface needed by an out-of-repository IBlazorWebViewHandler: an untyped native-view event payload, public RootComponent manager lifecycle methods, and a public static-content-hot-reload facade. The API baseline updates, cross-flavor event-args shape, external non-friend test assembly, solution wiring, and design documentation are coherent with that goal.

Verdict

NEEDS_DISCUSSION (medium confidence). No concrete error-level defect was found, but the new API is not ready to freeze exactly as submitted. The trusted gate is inconclusive because its baseline detector could not resolve the new test project; this is not counted as a regression failure.

Actionable findings

  1. BlazorWebViewStaticContentHotReload.AttachToWebViewManagerIfEnabled returns void even though registration is asynchronous. Its documentation says duplicate attachment throws, but the internal implementation discards the returned task, so failures are unobservable and registration is not guaranteed complete before navigation. The new public API should return the registration Task, or its contract must explicitly describe fire-and-forget behavior.
  2. RootComponent.RemoveFromWebViewManagerAsync exposes the parameter name webviewManager, inconsistent with adjacent webViewManager; named-argument source compatibility makes this worth correcting before ship.
  3. The new RootComponent remarks require callers to use WebViewManager.Dispatcher, while WebViewManager already dispatches internally and built-in handlers call these methods directly. The guidance should be removed or corrected.
  4. NativeWebView has a public mutable setter and shares storage with the typed WebView property. One initialized-event subscriber can therefore replace the value observed by later subscribers. An init-only public setter with internal backing-field assignment preserves external handler initialization without exposing post-construction mutation.
  5. Hot-reload tests can report success while skipping their feature assertions when metadata updates are unavailable. Enabled-only coverage should be represented as skipped/misconfigured rather than a trivially passing branch.
  6. The fake external handler discards root-component registration tasks and incorrectly claims they complete synchronously. Its startup helper should be asynchronous so failures and ordering are observable.
  7. The collection-order test currently observes only the fake's own list, not successful manager registration; it should await registration and verify manager-visible removal.
  8. The parameter-forwarding test never observes the parameter value and therefore does not cover its stated behavior.
  9. Three startup tests do not dispose the returned WebViewManager, allowing renderer/static hot-reload subscriptions to survive across tests.

Scope and risk

The production changes are additive but affect public API contracts and process-wide static hot-reload state. No external-output classifier or trim/NativeAOT contract is involved. The most important pre-ship corrections are the asynchronous attach contract, immutable event payload, and public parameter naming; the remaining findings improve documentation and ensure the new external-consumer tests actually observe the behavior they claim.


🛠️ Try-Fix — Analysis & Comparison

Aggregate Try-Fix Results

Candidate 1 — Framework-Owned Orchestration Entry Point

Result: Blocked before implementation or testing.

The candidate would replace the PR's three independently public implementation seams with one framework-owned initialization/orchestration API, a generic native-view accessor, and only the request-time hot-reload hook. This changes the mechanism from exposing internals and documenting call order to enforcing the complete lifecycle order inside MAUI.

The baseline script rejected the pre-existing harness-owned dirty worktree before creating .github/.baseline-state.json. No files changed, the targeted test was not run, the diff is empty, and inline self-review recorded zero findings. Exact restoration completed through the required script and reported the expected no-state result.

Detailed report: ../try-fix-1/content.md
Attempt artifacts: attempt-1/

Candidate 2 — Dependency-Injected Capability Interfaces

Result: Blocked before implementation or testing.

The candidate would expose narrow injected capabilities for native-view publication, root-component registration, and static-content hot reload. MAUI-owned services would retain access to the internals, while external handlers would coordinate the capabilities themselves. This avoids both the PR's direct visibility widening and Candidate 1's single framework-owned orchestration call.

The independently executed baseline step again rejected the pre-existing harness-owned dirty worktree before creating .github/.baseline-state.json. No files changed, the targeted test was not run, the diff is empty, and inline self-review recorded zero findings. Exact restoration completed through the required script and reported the expected no-state result.

Detailed report: ../try-fix-2/content.md
Attempt artifacts: attempt-2/

Aggregate outcome

Two materially distinct alternatives were identified, but neither could be implemented or empirically evaluated under the try-fix safety contract. Both attempts are Blocked, not failed: the baseline script rejected pre-existing harness-owned .github/eng changes before producing its allow-list. No candidate source changes remain in the worktree, no tests ran, and no Pass is claimed.


🏁 Report — Final Recommendation

⚠️ Final Recommendation: REQUEST CHANGES

Winner: pr-plus-reviewer

pr-plus-reviewer retains the submitted PR's focused external-backend API design while correcting pre-ship contract and test-quality issues identified by the single expert review. Its required targeted command completed with 24 passed, 0 failed, and 4 explicitly skipped metadata-update tests.

Candidate comparison

Rank Candidate Implementation Validation Assessment
1 pr-plus-reviewer Submitted design plus one consolidated reviewer patch PASS: 24 passed, 0 failed, 4 skipped Best balance of minimal API surface, observable async behavior, immutable event payload, consistent public naming, and credible external-consumer tests.
2 pr Raw submitted fix Gate INCONCLUSIVE; no reliable pass/fail Sound overall architecture, but expert review found six warnings and three suggestions. The most important unresolved concerns are fire-and-forget hot-reload registration, public post-construction mutation of shared event args, and weak/no-op test paths.
3 (tie) try-fix-1 Proposed framework-owned orchestration API BLOCKED: no diff, no test run Potentially centralizes sequencing, but it remained a design sketch and was never implemented or empirically evaluated.
3 (tie) try-fix-2 Proposed dependency-injected capability interfaces BLOCKED: no diff, no test run Offers narrower capability contracts but adds abstraction and likewise has no implementation or validation evidence.

No candidate failed regression tests. Both try-fix candidates rank below implemented candidates because baseline safety blocked all edits and validation.

Why pr-plus-reviewer wins

  • Hot-reload attachment now returns an awaitable task (AttachToWebViewManagerIfEnabledAsync), so external handlers can ensure notifier registration completes before navigation and can observe duplicate-registration faults.
  • NativeWebView is init-only for external callers, preventing one event subscriber from changing the typed value observed by later subscribers while preserving built-in internal setters.
  • The public RootComponent parameter spelling is consistent before API freeze, and misleading dispatcher guidance is removed.
  • External-handler startup awaits root-component registration; manager disposal and manager-visible registration checks eliminate false-positive/order-dependent tests.
  • PublicAPI files and design documentation are aligned with the refined contracts.

Remaining uncertainty

The targeted Windows run compiled the candidate and passed every runnable test. Four hot-reload-enabled tests were explicitly skipped because this process was not launched with metadata-update support; CI's configured DOTNET_MODIFIABLE_ASSEMBLIES=debug path remains the evidence needed for those branches. The original trusted gate was infrastructure-inconclusive and is not treated as a candidate failure.

Because the winning changes are not present in the submitted PR HEAD, the PR should be updated with pr-plus-reviewer/reviewer.patch before approval.


🧭 Next Steps — reviewer changes required

The reviewer-enhanced candidate identified changes that are not yet in the submitted PR.

Why: The reviewer-refined PR preserves the submitted architecture while fixing observable async registration, event-argument mutability, public naming, and test-lifetime/coverage issues. Its targeted test project passed with 24 passed, 0 failed, and 4 metadata-update tests explicitly skipped.

Address the actionable findings in this review before merging.

@MauiBot MauiBot removed the s/agent-review-in-progress AI review is currently running for this PR label Aug 26, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 37 out of 37 changed files in this pull request and generated 2 comments.

Suppressed comments (1)

src/BlazorWebView/src/Maui/RootComponent.cs:49

  • Minor typo in the comment: missing space after the comma (XAML,we). This reads oddly in docs/comments and is easy to fix while touching the method.
			// As a characteristic of XAML,we can't rely on non-default constructors. So we have to

Comment on lines +11 to +13
/// Stands in for the <see cref="WebViewManager"/> an out-of-repo BlazorWebView backend would write.
/// It only uses public API, including MAUI's public <see cref="MauiBlazorDispatcher"/> adapter rather
/// than a copied <see cref="AspNetCore.Components.Dispatcher"/> implementation.
Comment on lines +6 to +8
/// Stands in for the MAUI <see cref="IDispatcher"/> an external backend resolves from its services and
/// hands to <see cref="AspNetCore.Components.WebView.Maui.MauiDispatcher"/>. It runs everything inline.
/// </summary>
… shape

Follow-up to the API design review.

Detach lifecycle. BlazorWebViewStaticContentHotReload gains
TryDetachFromWebViewManager(WebViewManager), returning null when nothing was
attached and otherwise a Task that completes once the notifier root component has
been removed. Removal is sequenced after the in-flight attach, so it cannot race
into "there is no root component with selector 'body::after'". Detaching is
idempotent and clears the weak attach entry, so a handler that is disconnected and
later reconnected can attach again -- previously the second attach silently
replayed the first task and the notifier was never re-registered. All four in-box
MAUI handlers now detach in their disconnect path before disposing the manager.

Duplicated shape removed. The internal MAUI-side TryReplaceResponseContent wrapper
is deleted. The Android, iOS, Tizen and Windows web view managers now call the
public TryGetUpdatedStaticContent directly and apply the result to their own
response state, so the in-box code path is literally the public seam rather than a
parallel convenience over it. The remaining ref-based helper on
StaticContentHotReloadManager is now reachable only from the
WEBVIEW2_WINFORMS/WEBVIEW2_WPF branch of WebView2WebViewManager, which cannot
reference the MAUI-only type.

In-box dogfooding is now 12 call sites: attach, detach and content lookup on each
of the four platforms.

Tests grow to 43. New coverage: detach removes the notifier, detach returns null
when nothing was attached, detach is idempotent, attach works again after detach,
detach rejects null, and an external handler tears down through the seam. Two
existing attach assertions were rewritten from reference identity to behavior --
AddRootComponentAsync returns Task.CompletedTask before a page is attached, so
comparing task instances was vacuous; they now assert that exactly one
registration exists by checking that exactly one removal succeeds.

Verified with PublicApiType=Validate (local Debug defaults to Generate, which does
not validate) on net11.0 and net11.0-android37.0; the API delta is byte-identical
across all six TFM folders.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 27, 2026 00:47
@Redth

Redth commented Aug 27, 2026

Copy link
Copy Markdown
Member Author

Ready for fresh review — ead6b77aa0

All review feedback is addressed across two commits (1d8d778a + ead6b77a). Summary of the final shape:

Blocking items

Query-style hot reload. No public ref-mutating API. The public surface is:

public static bool TryGetUpdatedStaticContent(
    string contentRootRelativePath, string requestAbsoluteUri,
    out Stream? content, out string? contentType);

Caller owns status code, headers and disposal, and gets a fresh stream per call. The MAUI-side ref wrapper has been deleted — the in-box managers now call this exact method and apply the result themselves. A reflection test asserts no public static member on the seam takes a by-ref non-out parameter.

Read-only PlatformWebView. public object? PlatformWebView { get; } with no setter, supplied only via the new BlazorWebViewInitializedEventArgs(object) ctor or the existing internal typed WebView setter, which now throws on a second write. Tests assert SetMethod is null and that two subscribers observe the same instance.

Hardening

MAUI-only surface. Gated on #if WEBVIEW2_MAUI. WPF and WinForms PublicAPI.Unshipped.txt are byte-for-byte back at their pre-PR baseline.

Attach/detach lifecycle, awaitable and dogfooded. TryAttachToWebViewManager and TryDetachFromWebViewManager both return Task? (null = nothing to do). Attach is idempotent per manager; detach is idempotent, sequences after any in-flight attach, and clears the weak entry so a reconnected handler can attach again. 12 in-box call sites — attach, detach and content on each of Android, iOS, Tizen, Windows — so the shipping path is the public API.

Public dispatcher adapter. MauiDispatcher is now public sealed with a null-checked ctor; the fake external WebViewManager in the test assembly is built on it.

Naming/casing. RemoveFromWebViewManagerAsync(WebViewManager webViewManager) matches AddToWebViewManagerAsync; a reflection test pins both parameter names.

Cache internals. Documented as intentionally internal, with rationale and a pointer to the public IBlazorWebView.StaticContentCacheControlProvider.

RootComponent

Kept as accepted, unchanged apart from the parameter rename and null-argument checks.

Validation

  • External fake-backend assembly with no InternalsVisibleTo (and a test that fails if one is added): 43 tests, green with hot reload both disabled and enabled (DOTNET_MODIFIABLE_ASSEMBLIES=debug).
  • Existing MauiBlazorWebView.UnitTests 46/46 green.
  • -p:PublicApiType=Validate clean on net11.0 and net11.0-android37.0; API delta byte-identical across all six TFM folders. (Worth noting for other reviewers: local Debug builds default to PublicApiType=Generate and do not validate.)
  • Event args compile-verified under all five define shapes: WEBVIEW2_MAUI, WINDOWS+WEBVIEW2_MAUI, WINDOWS+WEBVIEW2_WPF, WINDOWS+WEBVIEW2_WINFORMS, TIZEN+WEBVIEW2_MAUI.

One behavior fix worth calling out explicitly: the previous attach idempotence had a latent bug where a reconnected handler would silently never re-register the notifier, because the cached task was replayed. Detach now clears that entry, and there's a regression test for re-attach.

Windows and Tizen full builds and the Blazor trimming/integration legs are CI-only from my macOS box.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 37 out of 37 changed files in this pull request and generated 4 comments.

Suppressed comments (3)

Previously missed (1) — in code that hasn't changed since the last review.

src/BlazorWebView/src/SharedSource/BlazorWebViewInitializedEventArgs.cs:125

  • SetPlatformWebView currently allows null, which means a handler can accidentally call the internal WebView setter with null and bypass the “already been set” guard (since _platformWebView stays null). That breaks the intended write-once semantics and diverges from the public constructor, which rejects null.
			if (_platformWebView is not null)
			{
				throw new InvalidOperationException(
					$"The platform web view for this {nameof(BlazorWebViewInitializedEventArgs)} has already been set.");
			}

src/BlazorWebView/tests/MauiBlazorWebView.ExternalHandler.UnitTests/FakeExternalWebViewManager.cs:14

  • This XML doc cref is missing the leading "Microsoft." and will be unresolved (e.g. CS1574) when documentation generation is enabled.
    src/BlazorWebView/tests/MauiBlazorWebView.ExternalHandler.UnitTests/FakeExternalDispatcher.cs:8
  • This XML doc cref is missing the leading "Microsoft." and will be unresolved (e.g. CS1574) when documentation generation is enabled.

Comment on lines +122 to +124
// Detach before disposal so a reconnected handler can attach the notifier again.
_ = BlazorWebViewStaticContentHotReload.TryDetachFromWebViewManager(_webviewManager);

Comment on lines +146 to +148
// Detach before disposal so a reconnected handler can attach the notifier again.
_ = BlazorWebViewStaticContentHotReload.TryDetachFromWebViewManager(_webviewManager);

Comment on lines +39 to +41
// Detach before disposal so a reconnected handler can attach the notifier again.
_ = BlazorWebViewStaticContentHotReload.TryDetachFromWebViewManager(_webviewManager);

Comment on lines +88 to +92
if (_webviewManager != null)
{
// Detach before teardown so a reconnected handler can attach the notifier again.
_ = BlazorWebViewStaticContentHotReload.TryDetachFromWebViewManager(_webviewManager);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-blazor Blazor Hybrid / Desktop, BlazorWebView

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants