Skip to content

Ability to monitor Blazor Server circuit activity #46968

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Mar 17, 2023
Merged
65 changes: 51 additions & 14 deletions src/Components/Server/src/Circuits/CircuitHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ internal partial class CircuitHost : IAsyncDisposable
private readonly CircuitHandler[] _circuitHandlers;
private readonly RemoteNavigationManager _navigationManager;
private readonly ILogger _logger;
private readonly Func<Func<Task>, Task> _dispatchInboundEvent;
private bool _initialized;
private bool _disposed;

Expand Down Expand Up @@ -66,6 +67,8 @@ public CircuitHost(
Circuit = new Circuit(this);
Handle = new CircuitHandle() { CircuitHost = this, };

_dispatchInboundEvent = BuildInboundEventDispatcher(_circuitHandlers, Circuit);

// An unhandled exception from the renderer is always fatal because it came from user code.
Renderer.UnhandledException += ReportAndInvoke_UnhandledException;
Renderer.UnhandledSynchronizationException += SynchronizationContext_UnhandledException;
Expand Down Expand Up @@ -324,7 +327,7 @@ public async Task OnRenderCompletedAsync(long renderId, string errorMessageOrNul

try
{
_ = Renderer.OnRenderCompletedAsync(renderId, errorMessageOrNull);
_ = DispatchInboundEventAsync(() => Renderer.OnRenderCompletedAsync(renderId, errorMessageOrNull));
}
catch (Exception e)
{
Expand All @@ -345,12 +348,12 @@ public async Task BeginInvokeDotNetFromJS(string callId, string assemblyName, st

try
{
await Renderer.Dispatcher.InvokeAsync(() =>
await DispatchInboundEventAsync(() => Renderer.Dispatcher.InvokeAsync(() =>
{
Log.BeginInvokeDotNet(_logger, callId, assemblyName, methodIdentifier, dotNetObjectId);
var invocationInfo = new DotNetInvocationInfo(assemblyName, methodIdentifier, dotNetObjectId, callId);
DotNetDispatcher.BeginInvokeDotNet(JSRuntime, invocationInfo, argsJson);
});
}));
}
catch (Exception ex)
{
Expand All @@ -371,7 +374,7 @@ public async Task EndInvokeJSFromDotNet(long asyncCall, bool succeeded, string a

try
{
await Renderer.Dispatcher.InvokeAsync(() =>
await DispatchInboundEventAsync(() => Renderer.Dispatcher.InvokeAsync(() =>
{
if (!succeeded)
{
Expand All @@ -384,7 +387,7 @@ await Renderer.Dispatcher.InvokeAsync(() =>
}

DotNetDispatcher.EndInvokeJS(JSRuntime, arguments);
});
}));
}
catch (Exception ex)
{
Expand All @@ -405,11 +408,11 @@ internal async Task ReceiveByteArray(int id, byte[] data)

try
{
await Renderer.Dispatcher.InvokeAsync(() =>
await DispatchInboundEventAsync(() => Renderer.Dispatcher.InvokeAsync(() =>
{
Log.ReceiveByteArraySuccess(_logger, id);
DotNetDispatcher.ReceiveByteArray(JSRuntime, id, data);
});
}));
}
catch (Exception ex)
{
Expand All @@ -430,10 +433,10 @@ internal async Task<bool> ReceiveJSDataChunk(long streamId, long chunkId, byte[]

try
{
return await Renderer.Dispatcher.InvokeAsync(() =>
return await DispatchInboundEventAsync(() => Renderer.Dispatcher.InvokeAsync(() =>
{
return RemoteJSDataStream.ReceiveData(JSRuntime, streamId, chunkId, chunk, error);
});
}));
}
catch (Exception ex)
{
Expand All @@ -453,7 +456,7 @@ public async Task<int> SendDotNetStreamAsync(DotNetStreamReference dotNetStreamR

try
{
return await Renderer.Dispatcher.InvokeAsync<int>(async () => await dotNetStreamReference.Stream.ReadAsync(buffer));
return await Renderer.Dispatcher.InvokeAsync(async () => await dotNetStreamReference.Stream.ReadAsync(buffer));
}
catch (Exception ex)
{
Expand Down Expand Up @@ -505,12 +508,12 @@ public async Task OnLocationChangedAsync(string uri, string state, bool intercep

try
{
await Renderer.Dispatcher.InvokeAsync(() =>
await DispatchInboundEventAsync(() => Renderer.Dispatcher.InvokeAsync(() =>
{
Log.LocationChange(_logger, uri, CircuitId);
_navigationManager.NotifyLocationChanged(uri, state, intercepted);
Log.LocationChangeSucceeded(_logger, uri, CircuitId);
});
}));
}

// It's up to the NavigationManager implementation to validate the URI.
Expand Down Expand Up @@ -547,11 +550,11 @@ public async Task OnLocationChangingAsync(int callId, string uri, string? state,

try
{
var shouldContinueNavigation = await Renderer.Dispatcher.InvokeAsync(async () =>
var shouldContinueNavigation = await DispatchInboundEventAsync(() => Renderer.Dispatcher.InvokeAsync(async () =>
{
Log.LocationChanging(_logger, uri, CircuitId);
return await _navigationManager.HandleLocationChangingAsync(uri, state, intercepted);
});
}));

await Client.SendAsync("JS.EndLocationChanging", callId, shouldContinueNavigation);
}
Expand Down Expand Up @@ -589,6 +592,40 @@ public void SendPendingBatches()
_ = Renderer.Dispatcher.InvokeAsync(Renderer.ProcessBufferedRenderBatches);
}

// Internal for testing.
internal Task DispatchInboundEventAsync(Func<Task> func)
=> _dispatchInboundEvent(func);

// Internal for testing.
internal async Task<TResult> DispatchInboundEventAsync<TResult>(Func<Task<TResult>> func)
{
TResult result = default;
await _dispatchInboundEvent(async () => result = await func());
return result;
}

private static Func<Func<Task>, Task> BuildInboundEventDispatcher(IReadOnlyList<CircuitHandler> circuitHandlers, Circuit circuit)
{
Func<CircuitInboundEventContext, Task>? result = null;

for (var i = circuitHandlers.Count - 1; i >= 0; i--)
{
if (circuitHandlers[i] is IHandleCircuitEvent inboundEventHandler)
{
var next = result ?? (static (context) => context.Handler());
result = (context) => inboundEventHandler.HandleInboundEventAsync(context, next);
}
}

if (result is null)
{
// If there are no registered handlers, there is no need to allocate a context on each call.
return static (task) => task();
}

return (task) => result(new(task, circuit));
}

private void AssertInitialized()
{
if (!_initialized)
Expand Down
23 changes: 23 additions & 0 deletions src/Components/Server/src/Circuits/CircuitInboundEventContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Components.Server.Circuits;

/// <summary>
/// Contains information about an inbound <see cref="Circuits.Circuit"/> event.
/// </summary>
public sealed class CircuitInboundEventContext
{
internal Func<Task> Handler { get; }

/// <summary>
/// Gets the <see cref="Circuits.Circuit"/> associated with the event.
/// </summary>
public Circuit Circuit { get; }

internal CircuitInboundEventContext(Func<Task> handler, Circuit circuit)
{
Handler = handler;
Circuit = circuit;
}
}
18 changes: 18 additions & 0 deletions src/Components/Server/src/Circuits/IHandleCircuitEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Components.Server.Circuits;

/// <summary>
/// A handler to process inbound circuit events.
/// </summary>
public interface IHandleCircuitEvent
{
/// <summary>
/// Invoked when inbound event on the circuit causes an asynchronous task to be dispatched on the server.
/// </summary>
/// <param name="context">The <see cref="CircuitInboundEventContext"/>.</param>
/// <param name="next">The next handler to invoke.</param>
/// <returns>A <see cref="Task"/> that completes when the event has finished.</returns>
Task HandleInboundEventAsync(CircuitInboundEventContext context, Func<CircuitInboundEventContext, Task> next);
}
4 changes: 4 additions & 0 deletions src/Components/Server/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
#nullable enable
Microsoft.AspNetCore.Components.Server.Circuits.CircuitInboundEventContext
Microsoft.AspNetCore.Components.Server.Circuits.CircuitInboundEventContext.Circuit.get -> Microsoft.AspNetCore.Components.Server.Circuits.Circuit!
Microsoft.AspNetCore.Components.Server.Circuits.IHandleCircuitEvent
Microsoft.AspNetCore.Components.Server.Circuits.IHandleCircuitEvent.HandleInboundEventAsync(Microsoft.AspNetCore.Components.Server.Circuits.CircuitInboundEventContext! context, System.Func<Microsoft.AspNetCore.Components.Server.Circuits.CircuitInboundEventContext!, System.Threading.Tasks.Task!>! next) -> System.Threading.Tasks.Task!
77 changes: 77 additions & 0 deletions src/Components/Server/test/Circuits/CircuitHostTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,83 @@ public async Task DisposeAsync_InvokesCircuitHandler()
handler2.VerifyAll();
}

[Fact]
public async Task DispatchInboundEventAsync_InvokesCircuitEventHandlers()
{
// Arrange
var handler1 = new Mock<CircuitHandler>(MockBehavior.Strict);
var handler2 = new Mock<CircuitHandler>(MockBehavior.Strict);
var handler3 = new Mock<CircuitHandler>(MockBehavior.Strict);
var sequence = new MockSequence();

// We deliberately avoid making handler2 an inbound event handler
var inboundEventHandler1 = handler1.As<IHandleCircuitEvent>();
var inboundEventHandler3 = handler3.As<IHandleCircuitEvent>();

var asyncLocal1 = new AsyncLocal<bool>();
var asyncLocal3 = new AsyncLocal<bool>();

inboundEventHandler1
.InSequence(sequence)
.Setup(h => h.HandleInboundEventAsync(It.IsAny<CircuitInboundEventContext>(), It.IsAny<Func<CircuitInboundEventContext, Task>>()))
.Returns(async (CircuitInboundEventContext context, Func<CircuitInboundEventContext, Task> next) =>
{
asyncLocal1.Value = true;
await next(context);
})
.Verifiable();

inboundEventHandler3
.InSequence(sequence)
.Setup(h => h.HandleInboundEventAsync(It.IsAny<CircuitInboundEventContext>(), It.IsAny<Func<CircuitInboundEventContext, Task>>()))
.Returns(async (CircuitInboundEventContext context, Func<CircuitInboundEventContext, Task> next) =>
{
asyncLocal3.Value = true;
await next(context);
})
.Verifiable();

var circuitHost = TestCircuitHost.Create(handlers: new[] { handler1.Object, handler2.Object, handler3.Object });
var asyncLocal1ValueInHandler = false;
var asyncLocal3ValueInHandler = false;

// Act
await circuitHost.DispatchInboundEventAsync(() =>
{
asyncLocal1ValueInHandler = asyncLocal1.Value;
asyncLocal3ValueInHandler = asyncLocal3.Value;
return Task.CompletedTask;
});

// Assert
inboundEventHandler1.VerifyAll();
inboundEventHandler3.VerifyAll();

Assert.False(asyncLocal1.Value);
Assert.False(asyncLocal3.Value);

Assert.True(asyncLocal1ValueInHandler);
Assert.True(asyncLocal3ValueInHandler);
}

[Fact]
public async Task DispatchInboundEventAsync_InvokesEventFunc_WhenNoCircuitEventHandlersAreRegistered()
{
// Arrange
var circuitHost = TestCircuitHost.Create();
var wasEventFuncInvoked = false;

// Act
await circuitHost.DispatchInboundEventAsync(() =>
{
wasEventFuncInvoked = true;
return Task.CompletedTask;
});

// Assert
Assert.True(wasEventFuncInvoked);
}

private static TestRemoteRenderer GetRemoteRenderer()
{
var serviceCollection = new ServiceCollection();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Components.TestServer;
using Microsoft.AspNetCore.Components.E2ETest;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.E2ETesting;
using OpenQA.Selenium;
using TestServer;
using Xunit.Abstractions;

namespace Microsoft.AspNetCore.Components.E2ETests.ServerExecutionTests;

public class CircuitContextTest : ServerTestBase<BasicTestAppServerSiteFixture<ServerStartup>>
{
public CircuitContextTest(
BrowserFixture browserFixture,
BasicTestAppServerSiteFixture<ServerStartup> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
}

protected override void InitializeAsyncCore()
{
Navigate(ServerPathBase, noReload: false);
Browser.MountTestComponent<CircuitContextComponent>();
Browser.Equal("Circuit Context", () => Browser.Exists(By.TagName("h1")).Text);
}

[Fact]
public void ComponentMethods_HaveCircuitContext()
{
Browser.Click(By.Id("trigger-click-event-button"));

Browser.True(() => HasCircuitContext("SetParametersAsync"));
Browser.True(() => HasCircuitContext("OnInitializedAsync"));
Browser.True(() => HasCircuitContext("OnParametersSetAsync"));
Browser.True(() => HasCircuitContext("OnAfterRenderAsync"));
Browser.True(() => HasCircuitContext("InvokeDotNet"));
Browser.True(() => HasCircuitContext("OnClickEvent"));

bool HasCircuitContext(string eventName)
{
var resultText = Browser.FindElement(By.Id($"circuit-context-result-{eventName}")).Text;
var result = bool.Parse(resultText);
return result;
}
}
}
1 change: 1 addition & 0 deletions src/Components/test/testassets/BasicTestApp/Index.razor
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<option value="BasicTestApp.AuthTest.CascadingAuthenticationStateParent">Cascading authentication state</option>
<option value="BasicTestApp.BindCasesComponent">bind cases</option>
<option value="BasicTestApp.CascadingValueTest.CascadingValueSupplier">Cascading values</option>
<option value="@GetTestServerProjectComponent("Components.TestServer.CircuitContextComponent")">Circuit context</option>
<option value="BasicTestApp.ComponentRefComponent">Component ref component</option>
<option value="BasicTestApp.ConcurrentRenderParent">Concurrent rendering</option>
<option value="BasicTestApp.ConfigurationComponent">Configuration</option>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
</div>

<!-- Used for specific test cases -->
<script src="js/circuitContextTest.js"></script>
<script src="js/jsinteroptests.js"></script>
<script src="js/renderattributestest.js"></script>
<script src="js/webComponentPerformingJsInterop.js"></script>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
window.circuitContextTest = {
invokeDotNetMethod: async (dotNetObject) => {
await dotNetObject.invokeMethodAsync('InvokeDotNet');
},
};
Loading