Skip to content
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

Ability to monitor Blazor Server circuit activity #46968

Merged
merged 13 commits into from
Mar 17, 2023
59 changes: 45 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 CircuitInboundEventDelegate _dispatchInboundEvent;
private bool _initialized;
private bool _disposed;

Expand Down Expand Up @@ -61,6 +62,8 @@ public CircuitHost(
_circuitHandlers = circuitHandlers ?? throw new ArgumentNullException(nameof(circuitHandlers));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));

_dispatchInboundEvent = BuildInboundEventDispatcher(_circuitHandlers);

Services = scope.ServiceProvider;

Circuit = new Circuit(this);
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,34 @@ public void SendPendingBatches()
_ = Renderer.Dispatcher.InvokeAsync(Renderer.ProcessBufferedRenderBatches);
}

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

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

private static CircuitInboundEventDelegate BuildInboundEventDispatcher(IReadOnlyList<CircuitHandler> circuitHandlers)
{
CircuitInboundEventDelegate result = (in CircuitInboundEventContext context) => context.Handler();

for (var i = circuitHandlers.Count - 1; i >= 0; i--)
{
if (circuitHandlers[i] is ICircuitInboundEventHandler inboundEventHandler)
{
var next = result;
result = (in CircuitInboundEventContext context) => inboundEventHandler.HandleInboundEventAsync(context, next);
}
}

return result;
}

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 readonly struct 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;
}
}
11 changes: 11 additions & 0 deletions src/Components/Server/src/Circuits/CircuitInboundEventDelegate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// 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 function that handles an inbound <see cref="Circuit"/> event.
/// </summary>
/// <param name="context">The <see cref="CircuitInboundEventContext"/> for the event.</param>
/// <returns>A <see cref="Task"/> that completes when the event has finished.</returns>
public delegate Task CircuitInboundEventDelegate(in CircuitInboundEventContext context);
18 changes: 18 additions & 0 deletions src/Components/Server/src/Circuits/ICircuitInboundEventHandler.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 ICircuitInboundEventHandler
{
/// <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, CircuitInboundEventDelegate next);
}
6 changes: 6 additions & 0 deletions src/Components/Server/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
#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.CircuitInboundEventContext.CircuitInboundEventContext() -> void
Microsoft.AspNetCore.Components.Server.Circuits.CircuitInboundEventDelegate
Microsoft.AspNetCore.Components.Server.Circuits.ICircuitInboundEventHandler
Microsoft.AspNetCore.Components.Server.Circuits.ICircuitInboundEventHandler.HandleInboundEventAsync(Microsoft.AspNetCore.Components.Server.Circuits.CircuitInboundEventContext context, Microsoft.AspNetCore.Components.Server.Circuits.CircuitInboundEventDelegate! next) -> System.Threading.Tasks.Task!
60 changes: 60 additions & 0 deletions src/Components/Server/test/Circuits/CircuitHostTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Reflection;
using System.Runtime.InteropServices;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -318,6 +319,65 @@ public async Task DisposeAsync_InvokesCircuitHandler()
handler2.VerifyAll();
}

[Fact]
public async Task DispatchInboundEventAsync_InvokesCircuitInboundEventHandlers()
{
// 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<ICircuitInboundEventHandler>();
var inboundEventHandler3 = handler3.As<ICircuitInboundEventHandler>();

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

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

inboundEventHandler3
.InSequence(sequence)
.Setup(h => h.HandleInboundEventAsync(It.IsAny<CircuitInboundEventContext>(), It.IsAny<CircuitInboundEventDelegate>()))
.Returns(async (CircuitInboundEventContext context, CircuitInboundEventDelegate 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);
}

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_HaveAsyncCircuitContext()
{
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');
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace ComponentsApp.Server;

internal class LoggingCircuitHandler : CircuitHandler
internal class LoggingCircuitHandler : CircuitHandler, ICircuitInboundEventHandler
{
private readonly ILogger<LoggingCircuitHandler> _logger;
private static Action<ILogger, string, Exception> _circuitOpened;
Expand Down Expand Up @@ -61,4 +61,9 @@ public override Task OnCircuitClosedAsync(Circuit circuit, CancellationToken cts
_circuitClosed(_logger, circuit.Id, null);
return base.OnCircuitClosedAsync(circuit, cts);
}

public Task HandleInboundEventAsync(CircuitInboundEventContext context, CircuitInboundEventDelegate next)
{
throw new NotImplementedException();
}
}
Loading