Skip to content

Commit

Permalink
Introduce CircuitHandler to handle circuit lifetime events (#6971)
Browse files Browse the repository at this point in the history
Introduce CircuitHandler to handle circuit lifetime events

Partial fix to #6353
  • Loading branch information
pranavkm authored Feb 4, 2019
2 parents e2c67ba + 62d10bc commit 2e54d64
Show file tree
Hide file tree
Showing 17 changed files with 369 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public static IApplicationBuilder UseRazorComponents<TStartup>(
// add SignalR and BlazorHub automatically.
if (options.UseSignalRWithBlazorHub)
{
builder.UseSignalR(route => route.MapHub<BlazorHub>(BlazorHub.DefaultPath));
builder.UseSignalR(route => route.MapHub<ComponentsHub>(ComponentsHub.DefaultPath));
}

// Use embedded static content for /_framework
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

namespace Microsoft.AspNetCore.Components.Hosting
{
internal class ServerSideBlazorApplicationBuilder : IComponentsApplicationBuilder
internal class ServerSideComponentsApplicationBuilder : IComponentsApplicationBuilder
{
public ServerSideBlazorApplicationBuilder(IServiceProvider services)
public ServerSideComponentsApplicationBuilder(IServiceProvider services)
{
Services = services;
Entries = new List<(Type componentType, string domElementSelector)>();
Expand Down
24 changes: 6 additions & 18 deletions src/Components/Server/src/Circuits/Circuit.cs
Original file line number Diff line number Diff line change
@@ -1,35 +1,23 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using Microsoft.JSInterop;

namespace Microsoft.AspNetCore.Components.Server.Circuits
{
/// <summary>
/// Represents an active connection between a Blazor server and a client.
/// Represents a link between a ASP.NET Core Component on the server and a client.
/// </summary>
public class Circuit
public sealed class Circuit
{
/// <summary>
/// Gets the current <see cref="Circuit"/>.
/// </summary>
public static Circuit Current => CircuitHost.Current?.Circuit;
private readonly CircuitHost _circuitHost;

internal Circuit(CircuitHost circuitHost)
{
JSRuntime = circuitHost.JSRuntime;
Services = circuitHost.Services;
_circuitHost = circuitHost;
}

/// <summary>
/// Gets the <see cref="IJSRuntime"/> associated with this circuit.
/// </summary>
public IJSRuntime JSRuntime { get; }

/// <summary>
/// Gets the <see cref="IServiceProvider"/> associated with this circuit.
/// Gets the identifier for the <see cref="Circuit"/>.
/// </summary>
public IServiceProvider Services { get; }
public string Id => _circuitHost.CircuitId;
}
}
81 changes: 81 additions & 0 deletions src/Components/Server/src/Circuits/CircuitHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.AspNetCore.Components.Server.Circuits
{
/// <summary>
/// A <see cref="CircuitHandler"/> allows running code during specific lifetime events of a <see cref="Circuit"/>.
/// <list type="bullet">
/// <item>
/// <see cref="OnCircuitOpenedAsync(Circuit, CancellationToken)"/> is invoked after an initial circuit to the client
/// has been established.
/// </item>
/// <item>
/// <see cref="OnConnectionUpAsync(Circuit, CancellationToken)(Circuit, CancellationToken)"/> is invoked immediately after the completion of
/// <see cref="OnCircuitOpenedAsync(Circuit, CancellationToken)"/>. In addition, the method is invoked each time a connection is re-established
/// with a client after it's been dropped. <see cref="OnConnectionDownAsync(Circuit, CancellationToken)"/> is invoked each time a connection
/// is dropped.
/// </item>
/// <item>
/// <see cref="OnCircuitClosedAsync(Circuit, CancellationToken)"/> is invoked prior to the server evicting the circuit to the client.
/// Application users may use this event to save state for a client that can be later rehydrated.
/// </item>
/// </list>
/// <ol>
/// </summary>
public abstract class CircuitHandler
{
/// <summary>
/// Gets the execution order for the current instance of <see cref="CircuitHandler"/>.
/// <para>
/// When multiple <see cref="CircuitHandler"/> instances are registered, the <see cref="Order"/>
/// property is used to determine the order in which instances are executed. When two handlers
/// have the same value for <see cref="Order"/>, their execution order is non-deterministic.
/// </para>
/// </summary>
/// <value>
/// Defaults to 0.
/// </value>
public virtual int Order => 0;

/// <summary>
/// Invoked when a new circuit was established.
/// </summary>
/// <param name="circuit">The <see cref="Circuit"/>.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that notifies when the client connection is aborted.</param>
/// <returns><see cref="Task"/> that represents the asynchronous execution operation.</returns>
public virtual Task OnCircuitOpenedAsync(Circuit circuit, CancellationToken cancellationToken) => Task.CompletedTask;

/// <summary>
/// Invoked when a connection to the client was established.
/// <para>
/// This method is executed once initially after <see cref="OnCircuitOpenedAsync(Circuit, CancellationToken)"/>
/// and once each for each reconnect during the lifetime of a circuit.
/// </para>
/// </summary>
/// <param name="circuit">The <see cref="Circuit"/>.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that notifies when the client connection is aborted.</param>
/// <returns><see cref="Task"/> that represents the asynchronous execution operation.</returns>
public virtual Task OnConnectionUpAsync(Circuit circuit, CancellationToken cancellationToken) => Task.CompletedTask;

/// <summary>
/// Invoked when a connection to the client was dropped.
/// </summary>
/// <param name="circuit">The <see cref="Circuit"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
/// <returns><see cref="Task"/> that represents the asynchronous execution operation.</returns>
public virtual Task OnConnectionDownAsync(Circuit circuit, CancellationToken cancellationToken) => Task.CompletedTask;


/// <summary>
/// Invoked when a new circuit is being discarded.
/// </summary>
/// <param name="circuit">The <see cref="Circuit"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
/// <returns><see cref="Task"/> that represents the asynchronous execution operation.</returns>
public virtual Task OnCircuitClosedAsync(Circuit circuit, CancellationToken cancellationToken) => Task.CompletedTask;
}
}
63 changes: 45 additions & 18 deletions src/Components/Server/src/Circuits/CircuitHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@

namespace Microsoft.AspNetCore.Components.Server.Circuits
{
internal class CircuitHost : IDisposable
internal class CircuitHost : IAsyncDisposable
{
private static readonly AsyncLocal<CircuitHost> _current = new AsyncLocal<CircuitHost>();
private readonly IServiceScope _scope;
private readonly CircuitHandler[] _circuitHandlers;
private bool _initialized;

private Action<IComponentsApplicationBuilder> _configure;

/// <summary>
/// Gets the current <see cref="Circuit"/>, if any.
Expand All @@ -37,25 +42,23 @@ public static void SetCurrentCircuitHost(CircuitHost circuitHost)
{
_current.Value = circuitHost ?? throw new ArgumentNullException(nameof(circuitHost));

Microsoft.JSInterop.JSRuntime.SetCurrentJSRuntime(circuitHost.JSRuntime);
JSInterop.JSRuntime.SetCurrentJSRuntime(circuitHost.JSRuntime);
RendererRegistry.SetCurrentRendererRegistry(circuitHost.RendererRegistry);
}

public event UnhandledExceptionEventHandler UnhandledException;

private bool _isInitialized;
private Action<IComponentsApplicationBuilder> _configure;

public CircuitHost(
IServiceScope scope,
IClientProxy client,
RendererRegistry rendererRegistry,
RemoteRenderer renderer,
Action<IComponentsApplicationBuilder> configure,
IJSRuntime jsRuntime,
CircuitSynchronizationContext synchronizationContext)
CircuitSynchronizationContext synchronizationContext,
CircuitHandler[] circuitHandlers)
{
Scope = scope ?? throw new ArgumentNullException(nameof(scope));
_scope = scope ?? throw new ArgumentNullException(nameof(scope));
Client = client ?? throw new ArgumentNullException(nameof(client));
RendererRegistry = rendererRegistry ?? throw new ArgumentNullException(nameof(rendererRegistry));
Renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
Expand All @@ -66,11 +69,14 @@ public CircuitHost(
Services = scope.ServiceProvider;

Circuit = new Circuit(this);
_circuitHandlers = circuitHandlers;

Renderer.UnhandledException += Renderer_UnhandledException;
SynchronizationContext.UnhandledException += SynchronizationContext_UnhandledException;
}

public string CircuitId { get; } = Guid.NewGuid().ToString();

public Circuit Circuit { get; }

public IClientProxy Client { get; }
Expand All @@ -81,30 +87,38 @@ public CircuitHost(

public RendererRegistry RendererRegistry { get; }

public IServiceScope Scope { get; }

public IServiceProvider Services { get; }

public CircuitSynchronizationContext SynchronizationContext { get; }

public async Task InitializeAsync()
public async Task InitializeAsync(CancellationToken cancellationToken)
{
await SynchronizationContext.Invoke(() =>
await SynchronizationContext.InvokeAsync(async () =>
{
SetCurrentCircuitHost(this);
var builder = new ServerSideBlazorApplicationBuilder(Services);
var builder = new ServerSideComponentsApplicationBuilder(Services);
_configure(builder);
for (var i = 0; i < builder.Entries.Count; i++)
{
var entry = builder.Entries[i];
Renderer.AddComponent(entry.componentType, entry.domElementSelector);
var (componentType, domElementSelector) = builder.Entries[i];
Renderer.AddComponent(componentType, domElementSelector);
}
for (var i = 0; i < _circuitHandlers.Length; i++)
{
await _circuitHandlers[i].OnCircuitOpenedAsync(Circuit, cancellationToken);
}
for (var i = 0; i < _circuitHandlers.Length; i++)
{
await _circuitHandlers[i].OnConnectionUpAsync(Circuit, cancellationToken);
}
});

_isInitialized = true;
_initialized = true;
}

public async void BeginInvokeDotNetFromJS(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson)
Expand All @@ -126,15 +140,28 @@ await SynchronizationContext.Invoke(() =>
}
}

public void Dispose()
public async ValueTask DisposeAsync()
{
Scope.Dispose();
await SynchronizationContext.InvokeAsync(async () =>
{
for (var i = 0; i < _circuitHandlers.Length; i++)
{
await _circuitHandlers[i].OnConnectionDownAsync(Circuit, default);
}
for (var i = 0; i < _circuitHandlers.Length; i++)
{
await _circuitHandlers[i].OnCircuitClosedAsync(Circuit, default);
}
});

_scope.Dispose();
Renderer.Dispose();
}

private void AssertInitialized()
{
if (!_isInitialized)
if (!_initialized)
{
throw new InvalidOperationException("Something is calling into the circuit before Initialize() completes");
}
Expand Down
10 changes: 8 additions & 2 deletions src/Components/Server/src/Circuits/DefaultCircuitFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Linq;
using Microsoft.AspNetCore.Components.Browser;
using Microsoft.AspNetCore.Components.Browser.Rendering;
using Microsoft.AspNetCore.Http;
Expand Down Expand Up @@ -33,7 +34,7 @@ public override CircuitHost CreateCircuitHost(HttpContext httpContext, IClientPr
{
if (!_options.StartupActions.TryGetValue(httpContext.Request.Path, out var config))
{
var message = $"Could not find a Blazor startup action for request path {httpContext.Request.Path}";
var message = $"Could not find an ASP.NET Core Components startup action for request path '{httpContext.Request.Path}'.";
throw new InvalidOperationException(message);
}

Expand All @@ -43,14 +44,19 @@ public override CircuitHost CreateCircuitHost(HttpContext httpContext, IClientPr
var synchronizationContext = new CircuitSynchronizationContext();
var renderer = new RemoteRenderer(scope.ServiceProvider, rendererRegistry, jsRuntime, client, synchronizationContext);

var circuitHandlers = scope.ServiceProvider.GetServices<CircuitHandler>()
.OrderBy(h => h.Order)
.ToArray();

var circuitHost = new CircuitHost(
scope,
client,
rendererRegistry,
renderer,
config,
jsRuntime,
synchronizationContext);
synchronizationContext,
circuitHandlers);

// Initialize per-circuit data that services need
(circuitHost.Services.GetRequiredService<IJSRuntimeAccessor>() as DefaultJSRuntimeAccessor).JSRuntime = jsRuntime;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,13 @@
using Microsoft.AspNetCore.Components.Builder;
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Components.Server.Circuits
namespace Microsoft.AspNetCore.Components.Server
{
internal class DefaultCircuitFactoryOptions
{
// During the DI configuration phase, we use Configure<DefaultCircuitFactoryOptions>(...)
// callbacks to build up this dictionary mapping paths to startup actions
public Dictionary<PathString, Action<IComponentsApplicationBuilder>> StartupActions { get; }

public DefaultCircuitFactoryOptions()
{
StartupActions = new Dictionary<PathString, Action<IComponentsApplicationBuilder>>();
}
internal Dictionary<PathString, Action<IComponentsApplicationBuilder>> StartupActions { get; }
= new Dictionary<PathString, Action<IComponentsApplicationBuilder>>();
}
}
2 changes: 1 addition & 1 deletion src/Components/Server/src/Circuits/RemoteUriHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public void Initialize(string uriAbsolute, string baseUriAbsolute)
[JSInvokable(nameof(NotifyLocationChanged))]
public static void NotifyLocationChanged(string uriAbsolute)
{
var circuit = Circuit.Current;
var circuit = CircuitHost.Current;
if (circuit == null)
{
var message = $"{nameof(NotifyLocationChanged)} called without a circuit.";
Expand Down
Loading

0 comments on commit 2e54d64

Please sign in to comment.