Skip to content

Commit a06e3a2

Browse files
mattleibowCopilot
andcommitted
Address AppType code review: robustness, trimming, cache, tests
Applies the actionable findings from the multi-model reviews of BlazorWebView.AppType: - Failure latch: set _appTypeRendered only after a successful render so a render failure no longer permanently latches a blank host page; a reconnect retries. - Symmetric setter: clearing AppType now restores the synthetic HostPage and resets the rendered state instead of leaving the view claiming a page it no longer serves. - Unique mount ids: each non-HeadOutlet interactive root gets a unique element id (app, app-1, ...) so a host document with multiple interactive boundaries no longer emits duplicate #app ids that collide on a single querySelector target. - Remove the dead MapAppType mapper entry + public method (MapHostPage/MapRootComponents already drive startup); drops it from the public API surface. - Trimming: keep a narrowly-scoped IL2072 suppression with an accurate justification. AppType is set via XAML reflection so it cannot carry a DynamicallyAccessedMembers annotation (that produces IL2111/IL2114); the component type is preserved by the XAML compiler and the Razor SDK trimming roots. - Manifest: correct the misleading 'readers complete synchronously' comment (Windows StorageFile is async), narrow the catch to IO/Json/NotImplemented/UnauthorizedAccess and log a corrupt manifest, and cache the immutable manifest for the process lifetime. - Stable LastModified on the in-memory host page IFileInfo. - MSBuild: warn when static web asset endpoints exist but zero fingerprinted entries are extracted (guards against a silent empty manifest), and guard the output directory. - Add unit tests for the manifest parser and the file provider, including the null-host-page fall-through (silent blank page) path. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2ca68fe1-bef6-46a6-a0fd-17b7654cef34
1 parent c5f0afd commit a06e3a2

14 files changed

Lines changed: 335 additions & 51 deletions

src/BlazorWebView/src/Maui/BlazorWebView.cs

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Microsoft.AspNetCore.Components.Web;
66
using Microsoft.Extensions.DependencyInjection;
77
using Microsoft.Extensions.FileProviders;
8+
using Microsoft.Extensions.Logging;
89
using Microsoft.Maui;
910
using Microsoft.Maui.Controls;
1011

@@ -59,6 +60,7 @@ public BlazorWebView()
5960

6061
private Type? _appType;
6162
private bool _appTypeRendered;
63+
private bool _syntheticHostPageApplied;
6264
private string? _renderedHostPageHtml;
6365

6466
/// <summary>
@@ -80,11 +82,30 @@ public Type? AppType
8082
{
8183
_appType = value;
8284

83-
// Provide a synthetic host page so the existing startup and relative-path logic flows
84-
// unchanged; the rendered document is overlaid onto the file provider at this path.
85-
if (value is not null && string.IsNullOrEmpty(HostPage))
85+
if (value is not null)
8686
{
87-
HostPage = AppTypeHostPage;
87+
// Provide a synthetic host page so the existing startup and relative-path logic
88+
// flows unchanged; the rendered document is overlaid onto the file provider at this
89+
// path. Only applied when the caller has not set an explicit HostPage.
90+
if (string.IsNullOrEmpty(HostPage))
91+
{
92+
HostPage = AppTypeHostPage;
93+
_syntheticHostPageApplied = true;
94+
}
95+
}
96+
else
97+
{
98+
// Clearing AppType: undo the synthetic host page and reset the rendered state so the
99+
// view does not keep claiming a host page it no longer serves (which would otherwise
100+
// leave a blank web view).
101+
if (_syntheticHostPageApplied)
102+
{
103+
HostPage = null;
104+
_syntheticHostPageApplied = false;
105+
}
106+
107+
_appTypeRendered = false;
108+
_renderedHostPageHtml = null;
88109
}
89110
}
90111
}
@@ -177,7 +198,8 @@ public virtual IFileProvider CreateFileProvider(string contentRootDir)
177198
// and fingerprinted-route serving work. The manifest lives outside the web root and is read
178199
// from the app package, so it is never served to the web view. Absent (or on platforms
179200
// without app-package access), fingerprinting simply stays off.
180-
var manifest = StaticWebAssetsManifest.TryLoad();
201+
var logger = Handler?.MauiContext?.Services?.GetService<ILoggerFactory>()?.CreateLogger<BlazorWebView>();
202+
var manifest = StaticWebAssetsManifest.TryLoad(logger);
181203

182204
// Render the host document once. This also collects any interactive components declared with
183205
// a render mode and registers them so they attach to the live document, and resolves @Assets
@@ -188,17 +210,22 @@ public virtual IFileProvider CreateFileProvider(string contentRootDir)
188210
return new BlazorWebViewFileProvider(platformFileProvider, hostPageRelativePath, _renderedHostPageHtml, manifest);
189211
}
190212

213+
// IL2072: AppType flows into HybridHostPageRenderer.Render's [DynamicallyAccessedMembers(All)]
214+
// parameter. AppType cannot itself be annotated: it is a public property set by XAML via
215+
// reflection (AppType="{x:Type components:App}"), and a DynamicallyAccessedMembers requirement on
216+
// a reflection-set property/parameter is not satisfiable by the trimmer (it produces IL2111/IL2114
217+
// instead). The assigned component type is preserved regardless: a {x:Type} reference is rooted by
218+
// the XAML compiler, and the interactive components it renders are rooted by the Razor SDK's
219+
// trimming roots (@rendermode / routable assembly), so their members survive trimming.
191220
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2072",
192-
Justification = "Blazor components referenced by AppType are preserved by the Razor SDK trimming roots, consistent with RootComponent.ComponentType.")]
221+
Justification = "AppType is set via XAML reflection so it cannot carry a DynamicallyAccessedMembers annotation; the component type is preserved by the XAML compiler ({x:Type}) and the Razor SDK trimming roots.")]
193222
private void EnsureAppTypeRendered(ResourceAssetCollection? assets)
194223
{
195224
if (_appTypeRendered || AppType is null)
196225
{
197226
return;
198227
}
199228

200-
_appTypeRendered = true;
201-
202229
var services = Handler?.MauiContext?.Services
203230
?? throw new InvalidOperationException($"Cannot render {nameof(AppType)} because no service provider is available.");
204231

@@ -213,6 +240,11 @@ private void EnsureAppTypeRendered(ResourceAssetCollection? assets)
213240
ComponentType = registration.ComponentType,
214241
});
215242
}
243+
244+
// Only latch success after the render and registration complete. If rendering throws (an
245+
// invalid AppType, a failing OnInitializedAsync, a missing service), the flag stays false so
246+
// a later handler reconnect retries instead of permanently serving a blank host page.
247+
_appTypeRendered = true;
216248
}
217249

218250
/// <summary>

src/BlazorWebView/src/Maui/BlazorWebViewFileProvider.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,18 +78,20 @@ private static string NormalizePath(string path) =>
7878
private sealed class InMemoryFileInfo : IFileInfo
7979
{
8080
private readonly byte[] _contents;
81+
private readonly DateTimeOffset _lastModified;
8182

8283
public InMemoryFileInfo(string name, byte[] contents)
8384
{
8485
Name = name;
8586
_contents = contents;
87+
_lastModified = DateTimeOffset.UtcNow;
8688
}
8789

8890
public bool Exists => true;
8991
public long Length => _contents.Length;
9092
public string? PhysicalPath => null;
9193
public string Name { get; }
92-
public DateTimeOffset LastModified => DateTimeOffset.UtcNow;
94+
public DateTimeOffset LastModified => _lastModified;
9395
public bool IsDirectory => false;
9496

9597
public Stream CreateReadStream() => new MemoryStream(_contents, writable: false);

src/BlazorWebView/src/Maui/BlazorWebViewHandler.cs

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ public partial class BlazorWebViewHandler : IBlazorWebViewHandler
3030
public static PropertyMapper<IBlazorWebView, BlazorWebViewHandler> BlazorWebViewMapper = new(ViewMapper)
3131
{
3232
[nameof(IBlazorWebView.HostPage)] = MapHostPage,
33-
[nameof(IBlazorWebView.AppType)] = MapAppType,
3433
[nameof(IBlazorWebView.RootComponents)] = MapRootComponents,
3534
#if WINDOWS
3635
[nameof(IView.FlowDirection)] = MapFlowDirection,
@@ -74,27 +73,6 @@ public static void MapHostPage(BlazorWebViewHandler handler, IBlazorWebView webV
7473
#endif
7574
}
7675

77-
/// <summary>
78-
/// Maps the <see cref="IBlazorWebView.AppType"/> property to the specified handler.
79-
/// </summary>
80-
/// <param name="handler">The <see cref="BlazorWebViewHandler"/>.</param>
81-
/// <param name="webView">The <see cref="IBlazorWebView"/>.</param>
82-
public static void MapAppType(BlazorWebViewHandler handler, IBlazorWebView webView)
83-
{
84-
#if !(NETSTANDARD || !PLATFORM)
85-
// Only views that opt into AppType need this mapper. When AppType is null the legacy
86-
// HostPage startup path is left completely untouched (MapHostPage already handled it).
87-
if (webView.AppType is null)
88-
{
89-
return;
90-
}
91-
92-
// AppType provides a synthetic HostPage, so ensure the handler picks it up and attempts startup.
93-
handler.HostPage = webView.HostPage;
94-
handler.StartWebViewCoreIfPossible();
95-
#endif
96-
}
97-
9876
/// <summary>
9977
/// Maps the <see cref="IBlazorWebView.RootComponents"/> property to the specified handler.
10078
/// </summary>

src/BlazorWebView/src/Maui/HybridHostPageRenderer.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ namespace Microsoft.AspNetCore.Components.WebView.Maui
3535
internal sealed class HybridHostPageRenderer : StaticHtmlRenderer
3636
{
3737
internal const string AppElementId = "app";
38-
internal const string AppSelector = "#" + AppElementId;
3938
internal const string HeadOutletSelector = "head::after";
4039

4140
private readonly List<HybridRootComponentRegistration> _registrations = new();
4241
private readonly ResourceAssetCollection _assets;
42+
private int _mountElementCount;
4343

4444
private HybridHostPageRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, ResourceAssetCollection assets)
4545
: base(serviceProvider, loggerFactory)
@@ -122,8 +122,13 @@ protected override IComponent ResolveComponentForRenderMode(
122122
}
123123

124124
// Any other interactive root becomes a mount element that the live component attaches to.
125-
_registrations.Add(new HybridRootComponentRegistration(AppSelector, componentType));
126-
return new HybridMountPlaceholder(AppElementId);
125+
// Each root gets a unique element id (app, app-1, app-2, ...) so a host document that
126+
// declares more than one interactive boundary does not emit duplicate ids that collide
127+
// on a single querySelector('#app') target.
128+
var elementId = _mountElementCount == 0 ? AppElementId : $"{AppElementId}-{_mountElementCount}";
129+
_mountElementCount++;
130+
_registrations.Add(new HybridRootComponentRegistration("#" + elementId, componentType));
131+
return new HybridMountPlaceholder(elementId);
127132
}
128133

129134
return base.ResolveComponentForRenderMode(componentType, parentComponentId, componentActivator, renderMode);

src/BlazorWebView/src/Maui/PublicAPI/net-android/PublicAPI.Unshipped.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,5 @@ Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler.CreateFilePro
1414
Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler.TryDispatchAsync(System.Action<System.IServiceProvider!>! workItem) -> System.Threading.Tasks.Task<bool>!
1515
Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions
1616
override Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewHandler.ConnectHandler(Android.Webkit.WebView! platformView) -> void
17-
static Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewHandler.MapAppType(Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewHandler! handler, Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebView! webView) -> void
1817
static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder, System.Func<System.IServiceProvider!, Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler!>! factory) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder!
1918
static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler<THandler>(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder!

src/BlazorWebView/src/Maui/PublicAPI/net-ios/PublicAPI.Unshipped.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,5 @@ Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler
1313
Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler.CreateFileProvider(string! contentRootDir) -> Microsoft.Extensions.FileProviders.IFileProvider!
1414
Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler.TryDispatchAsync(System.Action<System.IServiceProvider!>! workItem) -> System.Threading.Tasks.Task<bool>!
1515
Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions
16-
static Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewHandler.MapAppType(Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewHandler! handler, Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebView! webView) -> void
1716
static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder, System.Func<System.IServiceProvider!, Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler!>! factory) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder!
1817
static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler<THandler>(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder!

src/BlazorWebView/src/Maui/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,5 @@ Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler
1313
Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler.CreateFileProvider(string! contentRootDir) -> Microsoft.Extensions.FileProviders.IFileProvider!
1414
Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler.TryDispatchAsync(System.Action<System.IServiceProvider!>! workItem) -> System.Threading.Tasks.Task<bool>!
1515
Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions
16-
static Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewHandler.MapAppType(Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewHandler! handler, Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebView! webView) -> void
1716
static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder, System.Func<System.IServiceProvider!, Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler!>! factory) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder!
1817
static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler<THandler>(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder!

src/BlazorWebView/src/Maui/PublicAPI/net-tizen/PublicAPI.Unshipped.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,5 @@ Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler
1313
Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler.CreateFileProvider(string! contentRootDir) -> Microsoft.Extensions.FileProviders.IFileProvider!
1414
Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler.TryDispatchAsync(System.Action<System.IServiceProvider!>! workItem) -> System.Threading.Tasks.Task<bool>!
1515
Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions
16-
static Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewHandler.MapAppType(Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewHandler! handler, Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebView! webView) -> void
1716
static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder, System.Func<System.IServiceProvider!, Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler!>! factory) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder!
1817
static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler<THandler>(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder!

src/BlazorWebView/src/Maui/PublicAPI/net-windows/PublicAPI.Unshipped.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,5 @@ Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler
1313
Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler.CreateFileProvider(string! contentRootDir) -> Microsoft.Extensions.FileProviders.IFileProvider!
1414
Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler.TryDispatchAsync(System.Action<System.IServiceProvider!>! workItem) -> System.Threading.Tasks.Task<bool>!
1515
Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions
16-
static Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewHandler.MapAppType(Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewHandler! handler, Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebView! webView) -> void
1716
static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder, System.Func<System.IServiceProvider!, Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler!>! factory) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder!
1817
static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler<THandler>(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder!

src/BlazorWebView/src/Maui/PublicAPI/net/PublicAPI.Unshipped.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,5 @@ Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler
1313
Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler.CreateFileProvider(string! contentRootDir) -> Microsoft.Extensions.FileProviders.IFileProvider!
1414
Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler.TryDispatchAsync(System.Action<System.IServiceProvider!>! workItem) -> System.Threading.Tasks.Task<bool>!
1515
Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions
16-
static Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewHandler.MapAppType(Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewHandler! handler, Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebView! webView) -> void
1716
static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder, System.Func<System.IServiceProvider!, Microsoft.AspNetCore.Components.WebView.Maui.IBlazorWebViewHandler!>! factory) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder!
1817
static Microsoft.AspNetCore.Components.WebView.Maui.MauiBlazorWebViewBuilderExtensions.UsePlatformHandler<THandler>(this Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder! builder) -> Microsoft.AspNetCore.Components.WebView.Maui.IMauiBlazorWebViewBuilder!

0 commit comments

Comments
 (0)