|
using Microsoft.AspNetCore.Components.Rendering; |
|
using Microsoft.AspNetCore.Components; |
|
using Microsoft.AspNetCore.Diagnostics; |
|
using Microsoft.AspNetCore.Http.Features; |
|
using Microsoft.Extensions.Options; |
|
using System.Collections.Concurrent; |
|
using System.Diagnostics; |
|
using System.Diagnostics.CodeAnalysis; |
|
using System.Text; |
|
using System.Text.Json; |
|
using Microsoft.AspNetCore.Components.Web; |
|
using System.Reflection; |
|
using Microsoft.AspNetCore.Http; |
|
using Microsoft.AspNetCore.Builder; |
|
using Microsoft.AspNetCore.Hosting; |
|
using Microsoft.Extensions.Logging; |
|
using Microsoft.Extensions.DependencyInjection; |
|
|
|
namespace WebApplication01.Components; |
|
|
|
public partial class CustomErrorBoundary |
|
{ |
|
private static readonly ConcurrentDictionary<Type, bool> isBodyInInteractiveServerRenderMode = new(); |
|
private static readonly ConcurrentDictionary<Type, Type> surroundPageContentWithCustomErrorBoundaryInterceptorTypes = new(); |
|
|
|
public CustomErrorBoundary() |
|
{ |
|
ErrorContent = DefaultErrorContent; |
|
} |
|
|
|
private async Task<string> BuildExceptionPage(Exception ex) |
|
{ |
|
Task Next(HttpContext next) |
|
{ |
|
return Task.FromException(ex); |
|
} |
|
|
|
var middleware = new DeveloperExceptionPageMiddleware( |
|
Next, |
|
serviceProvider.GetRequiredService<IOptions<DeveloperExceptionPageOptions>>(), |
|
serviceProvider.GetRequiredService<ILoggerFactory>(), |
|
serviceProvider.GetRequiredService<IWebHostEnvironment>(), |
|
serviceProvider.GetRequiredService<DiagnosticSource>(), |
|
serviceProvider.GetServices<IDeveloperPageExceptionFilter>()); |
|
|
|
FeatureCollection features = []; |
|
var requestFeature = new HttpRequestFeature(); |
|
requestFeature.Headers.Accept = "text/html"; |
|
features.Set<IHttpRequestFeature>(requestFeature); |
|
features.Set<IHttpResponseFeature>( |
|
new HttpResponseFeature()); |
|
MemoryStream responseBody = new(); |
|
features.Set<IHttpResponseBodyFeature>( |
|
new StreamResponseBodyFeature(responseBody)); |
|
|
|
var context = new DefaultHttpContext(features); |
|
|
|
await middleware.Invoke(context); |
|
|
|
return Encoding.UTF8.GetString(responseBody.ToArray()); |
|
} |
|
|
|
private void UpdateDeveloperExceptionPageScript(string html) |
|
{ |
|
var json = JsonSerializer.Serialize(new { html }); |
|
DeveloperExceptionPageScript = $$""" |
|
(function (data) { |
|
document.documentElement.innerHTML = data.{{nameof(html)}}; |
|
|
|
for (const script of [ ... document.querySelectorAll("script") ]) { |
|
const newScript = document.createElement("script"); |
|
newScript.textContent = script.textContent; |
|
script.replaceWith(newScript); |
|
} |
|
})({{json}}); |
|
"""; |
|
|
|
StateHasChanged(); |
|
} |
|
|
|
/// <summary> |
|
/// With this method you can find out if the component is InteractiveServer, sadly i found no |
|
/// way to globally apply <CustomErrorBoundary>, because ChildContent can't be serialized. |
|
/// Here is someone else who tried it and was not able to: |
|
/// https://github.com/dotnet/aspnetcore/issues/56413#issuecomment-2322732045 |
|
/// You can not simply wrap the @Body with another component, instead look at the |
|
/// <see cref="SurroundPageContentWithCustomErrorBoundary"/> method, which generates a |
|
/// type on runtime to surround the page content with the <see cref="CustomErrorBoundary"/> |
|
/// </summary> |
|
private static bool IsBodyInInteractiveServerRenderMode([NotNullWhen(true)] RenderFragment? body) => |
|
body is { Target: RouteView { RouteData.PageType: var pageType } } |
|
&& isBodyInInteractiveServerRenderMode.GetOrAdd(pageType, |
|
static type => |
|
{ |
|
if (type.GetCustomAttributes() is { } attributes) |
|
{ |
|
foreach (var attribute in attributes) |
|
{ |
|
if (attribute is RenderModeAttribute { Mode: InteractiveServerRenderMode }) |
|
{ |
|
return true; |
|
} |
|
} |
|
} |
|
|
|
return false; |
|
}); |
|
|
|
/// <summary> |
|
/// You need to wrap the <see cref="RenderMode.InteractiveServer"/> page component type within the |
|
/// <see cref="ErrorBoundary"/> inside a new <see cref="RenderMode.InteractiveServer"/> component type. |
|
/// If a callback is handled by blazor it only saw the actual page component and skipped the containing |
|
/// <see cref="ErrorBoundary"/>. |
|
/// </summary> |
|
private static Type SurroundPageContentWithCustomErrorBoundary(Type pageComponentType) => |
|
surroundPageContentWithCustomErrorBoundaryInterceptorTypes.GetOrAdd(pageComponentType, |
|
static type => |
|
{ |
|
return typeof(ContentWrapper<>).MakeGenericType(type); |
|
}); |
|
|
|
private static RenderFragment WrapWithCustomErrorBoundary(RenderFragment body) |
|
{ |
|
if (body.Target is not RouteView |
|
{ |
|
RouteData: var routeData, |
|
DefaultLayout: var defaultLayout, |
|
}) |
|
return body; |
|
|
|
var newPageType = SurroundPageContentWithCustomErrorBoundary(routeData.PageType); |
|
|
|
#pragma warning disable IDE0079 // IDE tells us that BL0005 supression does have no effect, which is not the case |
|
#pragma warning disable BL0005 // Component parameter should not be set outside of its component. |
|
RouteView interceptingRouteView = new() |
|
{ |
|
DefaultLayout = defaultLayout, |
|
RouteData = new(newPageType, routeData.RouteValues) |
|
{ |
|
Template = routeData.Template, |
|
}, |
|
}; |
|
#pragma warning restore BL0005 // Component parameter should not be set outside of its component. |
|
#pragma warning restore IDE0079 // IDE tells us that BL0005 supression does have no effect, which is not the case |
|
|
|
var result = (RenderFragment)Delegate.CreateDelegate( |
|
type: typeof(RenderFragment), |
|
firstArgument: interceptingRouteView, |
|
method: body.Method); |
|
|
|
return result; |
|
} |
|
|
|
/// <summary> |
|
/// Wraps the content of the given body with the <see cref="CustomErrorBoundary"/>, if it |
|
/// is refering to a <see cref="RouteView"> with @rendermode <see cref="RenderMode.InteractiveServer">. |
|
/// </summary> |
|
public static RenderFragment? Wrap( |
|
RenderFragment? body) => |
|
IsBodyInInteractiveServerRenderMode(body) |
|
? WrapWithCustomErrorBoundary(body) |
|
: body; |
|
|
|
[__PrivateComponentRenderMode] |
|
private sealed class ContentWrapper<TComponent> : ComponentBase |
|
where TComponent : notnull, IComponent |
|
{ |
|
private IReadOnlyDictionary<string, object?>? capturedParameters; |
|
|
|
public override Task SetParametersAsync(ParameterView parameters) |
|
{ |
|
capturedParameters = parameters.ToDictionary(); |
|
return base.SetParametersAsync(ParameterView.Empty); |
|
} |
|
|
|
protected override void BuildRenderTree(RenderTreeBuilder builder) |
|
{ |
|
builder.OpenComponent<CustomErrorBoundary>(0); |
|
builder.AddAttribute(1, nameof(CustomErrorBoundary.ChildContent), |
|
(RenderFragment)(builder2 => |
|
{ |
|
builder2.OpenComponent(2, typeof(TComponent)); |
|
// Passing the route parameters to the actual page component. |
|
// The parameters are already in the correct shape, because the |
|
// RouteView was initially build for our TComponent. So no |
|
// further processing is needed. |
|
// Cascading values are as of .NET 9 not supported between static |
|
// and interactive render boundaries. It is intended, that we |
|
// use this wrapper inside the static Layout component. So we can |
|
// just ignore all cascading parameters for now. |
|
// I think there is nothing else what we need to pass here. |
|
// If you want to wrap other kinds of components, you may need |
|
// to figure out, if and what you need to extend here to make |
|
// them fully work. |
|
builder2.AddMultipleAttributes(3, capturedParameters!); |
|
builder2.CloseComponent(); |
|
})); |
|
builder.CloseComponent(); |
|
} |
|
} |
|
|
|
#pragma warning disable IDE1006 // Benennungsstile |
|
private sealed class __PrivateComponentRenderModeAttribute : RenderModeAttribute |
|
#pragma warning restore IDE1006 // Benennungsstile |
|
{ |
|
private static IComponentRenderMode ModeImpl => RenderMode.InteractiveServer; |
|
public override IComponentRenderMode Mode => ModeImpl; |
|
} |
|
} |