Terminate circuit on error
Fixes: #11845 See: https://github.com/aspnet/AspNetCore/pull/12857 for detailed notes.
This commit is contained in:
parent
9d25e33246
commit
18f5ec71ed
|
|
@ -75,16 +75,24 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
batchBuilder.UpdatedComponentDiffs.Append(diff);
|
||||
}
|
||||
|
||||
public void DisposeInBatch(RenderBatchBuilder batchBuilder)
|
||||
public bool TryDisposeInBatch(RenderBatchBuilder batchBuilder, out Exception exception)
|
||||
{
|
||||
_componentWasDisposed = true;
|
||||
exception = null;
|
||||
|
||||
// TODO: Handle components throwing during dispose. Shouldn't break the whole render batch.
|
||||
if (Component is IDisposable disposable)
|
||||
try
|
||||
{
|
||||
disposable.Dispose();
|
||||
if (Component is IDisposable disposable)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exception = ex;
|
||||
}
|
||||
|
||||
// We don't expect these things to throw.
|
||||
RenderTreeDiffBuilder.DisposeFrames(batchBuilder, CurrentRenderTree.GetFrames());
|
||||
|
||||
if (_hasAnyCascadingParameterSubscriptions)
|
||||
|
|
@ -93,13 +101,27 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
}
|
||||
|
||||
DisposeBuffers();
|
||||
|
||||
return exception == null;
|
||||
}
|
||||
|
||||
// Callers expect this method to always return a faulted task.
|
||||
public Task NotifyRenderCompletedAsync()
|
||||
{
|
||||
if (Component is IHandleAfterRender handlerAfterRender)
|
||||
{
|
||||
return handlerAfterRender.OnAfterRenderAsync();
|
||||
try
|
||||
{
|
||||
return handlerAfterRender.OnAfterRenderAsync();
|
||||
}
|
||||
catch (OperationCanceledException cex)
|
||||
{
|
||||
return Task.FromCanceled(cex.CancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Task.FromException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
|
|
|
|||
|
|
@ -587,16 +587,31 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
Log.RenderingComponent(_logger, componentState);
|
||||
componentState.RenderIntoBatch(_batchBuilder, renderQueueEntry.RenderFragment);
|
||||
|
||||
List<Exception> exceptions = null;
|
||||
|
||||
// Process disposal queue now in case it causes further component renders to be enqueued
|
||||
while (_batchBuilder.ComponentDisposalQueue.Count > 0)
|
||||
{
|
||||
var disposeComponentId = _batchBuilder.ComponentDisposalQueue.Dequeue();
|
||||
var disposeComponentState = GetRequiredComponentState(disposeComponentId);
|
||||
Log.DisposingComponent(_logger, disposeComponentState);
|
||||
disposeComponentState.DisposeInBatch(_batchBuilder);
|
||||
if (!disposeComponentState.TryDisposeInBatch(_batchBuilder, out var exception))
|
||||
{
|
||||
exceptions ??= new List<Exception>();
|
||||
exceptions.Add(exception);
|
||||
}
|
||||
_componentStateById.Remove(disposeComponentId);
|
||||
_batchBuilder.DisposedComponentIds.Append(disposeComponentId);
|
||||
}
|
||||
|
||||
if (exceptions?.Count > 1)
|
||||
{
|
||||
HandleException(new AggregateException("Exceptions were encountered while disposing components.", exceptions));
|
||||
}
|
||||
else if (exceptions?.Count == 1)
|
||||
{
|
||||
HandleException(exceptions[0]);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveEventHandlerIds(ArrayRange<ulong> eventHandlerIds, Task afterTaskIgnoreErrors)
|
||||
|
|
@ -681,6 +696,9 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// <param name="disposing"><see langword="true"/> if this method is being invoked by <see cref="IDisposable.Dispose"/>, otherwise <see langword="false"/>.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
// It's important that we handle all exceptions here before reporting any of them.
|
||||
// This way we can dispose all components before an error handler kicks in.
|
||||
List<Exception> exceptions = null;
|
||||
foreach (var componentState in _componentStateById.Values)
|
||||
{
|
||||
Log.DisposingComponent(_logger, componentState);
|
||||
|
|
@ -693,11 +711,21 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
HandleException(exception);
|
||||
exceptions ??= new List<Exception>();
|
||||
exceptions.Add(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_batchBuilder.Dispose();
|
||||
_batchBuilder.Dispose();
|
||||
|
||||
if (exceptions?.Count > 1)
|
||||
{
|
||||
HandleException(new AggregateException("Exceptions were encountered while disposing components.", exceptions));
|
||||
}
|
||||
else if (exceptions?.Count == 1)
|
||||
{
|
||||
HandleException(exceptions[0]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2046,6 +2046,127 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
Assert.Equal(2, renderer.Batches.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RenderBatch_HandlesExceptionsFromAllDisposedComponents()
|
||||
{
|
||||
// Arrange
|
||||
var renderer = new TestRenderer { ShouldHandleExceptions = true };
|
||||
var exception1 = new Exception();
|
||||
var exception2 = new Exception();
|
||||
|
||||
var firstRender = true;
|
||||
var component = new TestComponent(builder =>
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
builder.AddContent(0, "Hello");
|
||||
builder.OpenComponent<DisposableComponent>(1);
|
||||
builder.AddAttribute(1, nameof(DisposableComponent.DisposeAction), (Action)(() => throw exception1));
|
||||
builder.CloseComponent();
|
||||
|
||||
builder.OpenComponent<DisposableComponent>(2);
|
||||
builder.AddAttribute(1, nameof(DisposableComponent.DisposeAction), (Action)(() => throw exception2));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
});
|
||||
var componentId = renderer.AssignRootComponentId(component);
|
||||
component.TriggerRender();
|
||||
|
||||
// Act: Second render
|
||||
firstRender = false;
|
||||
component.TriggerRender();
|
||||
|
||||
// Assert: Applicable children are included in disposal list
|
||||
Assert.Equal(2, renderer.Batches.Count);
|
||||
Assert.Equal(new[] { 1, 2 }, renderer.Batches[1].DisposedComponentIDs);
|
||||
|
||||
// Outer component is still alive and not disposed.
|
||||
Assert.False(component.Disposed);
|
||||
var aex = Assert.IsType<AggregateException>(Assert.Single(renderer.HandledExceptions));
|
||||
Assert.Contains(exception1, aex.InnerExceptions);
|
||||
Assert.Contains(exception2, aex.InnerExceptions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RenderBatch_DoesNotDisposeComponentMultipleTimes()
|
||||
{
|
||||
// Arrange
|
||||
var renderer = new TestRenderer { ShouldHandleExceptions = true };
|
||||
var exception1 = new Exception();
|
||||
var exception2 = new Exception();
|
||||
|
||||
var count1 = 0;
|
||||
var count2 = 0;
|
||||
var count3 = 0;
|
||||
var count4 = 0;
|
||||
var count5 = 0;
|
||||
|
||||
var firstRender = true;
|
||||
var component = new TestComponent(builder =>
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
builder.AddContent(0, "Hello");
|
||||
builder.OpenComponent<DisposableComponent>(1);
|
||||
builder.AddAttribute(1, nameof(DisposableComponent.DisposeAction), (Action)(() => { count1++; }));
|
||||
builder.CloseComponent();
|
||||
|
||||
builder.OpenComponent<DisposableComponent>(2);
|
||||
builder.AddAttribute(1, nameof(DisposableComponent.DisposeAction), (Action)(() => { count2++; throw exception1; }));
|
||||
builder.CloseComponent();
|
||||
|
||||
builder.OpenComponent<DisposableComponent>(3);
|
||||
builder.AddAttribute(1, nameof(DisposableComponent.DisposeAction), (Action)(() => { count3++; }));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
builder.OpenComponent<DisposableComponent>(4);
|
||||
builder.AddAttribute(1, nameof(DisposableComponent.DisposeAction), (Action)(() => { count4++; throw exception2; }));
|
||||
builder.CloseComponent();
|
||||
|
||||
builder.OpenComponent<DisposableComponent>(5);
|
||||
builder.AddAttribute(1, nameof(DisposableComponent.DisposeAction), (Action)(() => { count5++; }));
|
||||
builder.CloseComponent();
|
||||
});
|
||||
var componentId = renderer.AssignRootComponentId(component);
|
||||
component.TriggerRender();
|
||||
|
||||
// Act: Second render
|
||||
firstRender = false;
|
||||
component.TriggerRender();
|
||||
|
||||
// Assert: Applicable children are included in disposal list
|
||||
Assert.Equal(2, renderer.Batches.Count);
|
||||
Assert.Equal(new[] { 1, 2, 3 }, renderer.Batches[1].DisposedComponentIDs);
|
||||
|
||||
// Components "disposed" in the batch were all disposed, components that are still live were not disposed
|
||||
Assert.Equal(1, count1);
|
||||
Assert.Equal(1, count2);
|
||||
Assert.Equal(1, count3);
|
||||
Assert.Equal(0, count4);
|
||||
Assert.Equal(0, count5);
|
||||
|
||||
// Outer component is still alive and not disposed.
|
||||
Assert.False(component.Disposed);
|
||||
var ex = Assert.IsType<Exception>(Assert.Single(renderer.HandledExceptions));
|
||||
Assert.Same(exception1, ex);
|
||||
|
||||
// Act: Dispose renderer
|
||||
renderer.Dispose();
|
||||
|
||||
Assert.Equal(2, renderer.HandledExceptions.Count);
|
||||
ex = renderer.HandledExceptions[1];
|
||||
Assert.Same(exception2, ex);
|
||||
|
||||
// Assert: Everything was disposed once.
|
||||
Assert.Equal(1, count1);
|
||||
Assert.Equal(1, count2);
|
||||
Assert.Equal(1, count3);
|
||||
Assert.Equal(1, count4);
|
||||
Assert.Equal(1, count5);
|
||||
Assert.True(component.Disposed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisposesEventHandlersWhenAttributeValueChanged()
|
||||
{
|
||||
|
|
@ -3059,7 +3180,7 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExceptionsThrownFromHandleAfterRender_AreHandled()
|
||||
public async Task ExceptionsThrownFromHandleAfterRender_Sync_AreHandled()
|
||||
{
|
||||
// Arrange
|
||||
var renderer = new TestRenderer { ShouldHandleExceptions = true };
|
||||
|
|
@ -3078,7 +3199,7 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
{
|
||||
new NestedAsyncComponent.ExecutionAction
|
||||
{
|
||||
Event = NestedAsyncComponent.EventType.OnAfterRenderAsync,
|
||||
Event = NestedAsyncComponent.EventType.OnAfterRenderAsyncSync,
|
||||
EventAction = () =>
|
||||
{
|
||||
throw exception;
|
||||
|
|
@ -3089,12 +3210,11 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
{
|
||||
new NestedAsyncComponent.ExecutionAction
|
||||
{
|
||||
Event = NestedAsyncComponent.EventType.OnAfterRenderAsync,
|
||||
EventAction = async () =>
|
||||
Event = NestedAsyncComponent.EventType.OnAfterRenderAsyncSync,
|
||||
EventAction = () =>
|
||||
{
|
||||
await Task.Yield();
|
||||
taskCompletionSource.TrySetResult(0);
|
||||
return (1, NestedAsyncComponent.EventType.OnAfterRenderAsync);
|
||||
return Task.FromResult((1, NestedAsyncComponent.EventType.OnAfterRenderAsyncSync));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -3113,6 +3233,137 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
Assert.Same(exception, Assert.Single(renderer.HandledExceptions).GetBaseException());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExceptionsThrownFromHandleAfterRender_Async_AreHandled()
|
||||
{
|
||||
// Arrange
|
||||
var renderer = new TestRenderer { ShouldHandleExceptions = true };
|
||||
var component = new NestedAsyncComponent();
|
||||
var exception = new InvalidTimeZoneException();
|
||||
|
||||
var taskCompletionSource = new TaskCompletionSource<int>();
|
||||
|
||||
// Act/Assert
|
||||
var componentId = renderer.AssignRootComponentId(component);
|
||||
var renderTask = renderer.RenderRootComponentAsync(componentId, ParameterView.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
[nameof(NestedAsyncComponent.EventActions)] = new Dictionary<int, IList<NestedAsyncComponent.ExecutionAction>>
|
||||
{
|
||||
[0] = new[]
|
||||
{
|
||||
new NestedAsyncComponent.ExecutionAction
|
||||
{
|
||||
Event = NestedAsyncComponent.EventType.OnAfterRenderAsyncAsync,
|
||||
EventAction = async () =>
|
||||
{
|
||||
await Task.Yield();
|
||||
throw exception;
|
||||
},
|
||||
}
|
||||
},
|
||||
[1] = new[]
|
||||
{
|
||||
new NestedAsyncComponent.ExecutionAction
|
||||
{
|
||||
Event = NestedAsyncComponent.EventType.OnAfterRenderAsyncAsync,
|
||||
EventAction = async () =>
|
||||
{
|
||||
await Task.Yield();
|
||||
taskCompletionSource.TrySetResult(0);
|
||||
return (1, NestedAsyncComponent.EventType.OnAfterRenderAsyncAsync);
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
[nameof(NestedAsyncComponent.WhatToRender)] = new Dictionary<int, Func<NestedAsyncComponent, RenderFragment>>
|
||||
{
|
||||
[0] = CreateRenderFactory(new[] { 1 }),
|
||||
[1] = CreateRenderFactory(Array.Empty<int>()),
|
||||
},
|
||||
}));
|
||||
|
||||
Assert.True(renderTask.IsCompletedSuccessfully);
|
||||
|
||||
// OnAfterRenderAsync happens in the background. Make it more predictable, by gating it until we're ready to capture exceptions.
|
||||
await taskCompletionSource.Task.TimeoutAfter(TimeSpan.FromSeconds(10));
|
||||
Assert.Same(exception, Assert.Single(renderer.HandledExceptions).GetBaseException());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExceptionThrownFromConstructor()
|
||||
{
|
||||
// Arrange
|
||||
var renderer = new TestRenderer { ShouldHandleExceptions = true };
|
||||
var component = new TestComponent(builder =>
|
||||
{
|
||||
builder.OpenComponent<ConstructorThrowingComponent>(0);
|
||||
builder.CloseComponent();
|
||||
});
|
||||
|
||||
// Act/Assert
|
||||
var componentId = renderer.AssignRootComponentId(component);
|
||||
var renderTask = renderer.RenderRootComponentAsync(componentId);
|
||||
|
||||
await renderTask;
|
||||
Assert.True(renderTask.IsCompletedSuccessfully);
|
||||
Assert.Same(ConstructorThrowingComponent.Exception, Assert.Single(renderer.HandledExceptions).GetBaseException());
|
||||
}
|
||||
|
||||
private class ConstructorThrowingComponent : IComponent
|
||||
{
|
||||
public static readonly Exception Exception = new InvalidTimeZoneException();
|
||||
|
||||
public ConstructorThrowingComponent()
|
||||
{
|
||||
throw Exception;
|
||||
}
|
||||
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExceptionThrownFromAttach()
|
||||
{
|
||||
// Arrange
|
||||
var renderer = new TestRenderer { ShouldHandleExceptions = true };
|
||||
var component = new TestComponent(builder =>
|
||||
{
|
||||
builder.OpenComponent<AttachThrowingComponent>(0);
|
||||
builder.CloseComponent();
|
||||
});
|
||||
|
||||
// Act/Assert
|
||||
var componentId = renderer.AssignRootComponentId(component);
|
||||
var renderTask = renderer.RenderRootComponentAsync(componentId);
|
||||
|
||||
await renderTask;
|
||||
Assert.True(renderTask.IsCompletedSuccessfully);
|
||||
Assert.Same(AttachThrowingComponent.Exception, Assert.Single(renderer.HandledExceptions).GetBaseException());
|
||||
}
|
||||
|
||||
private class AttachThrowingComponent : IComponent
|
||||
{
|
||||
public static readonly Exception Exception = new InvalidTimeZoneException();
|
||||
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
throw Exception;
|
||||
}
|
||||
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SynchronousCancelledTasks_HandleAfterRender_Works()
|
||||
{
|
||||
|
|
@ -3132,7 +3383,7 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
{
|
||||
new NestedAsyncComponent.ExecutionAction
|
||||
{
|
||||
Event = NestedAsyncComponent.EventType.OnAfterRenderAsync,
|
||||
Event = NestedAsyncComponent.EventType.OnAfterRenderAsyncAsync,
|
||||
EventAction = () => tcs.Task,
|
||||
}
|
||||
},
|
||||
|
|
@ -3166,7 +3417,7 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
{
|
||||
new NestedAsyncComponent.ExecutionAction
|
||||
{
|
||||
Event = NestedAsyncComponent.EventType.OnAfterRenderAsync,
|
||||
Event = NestedAsyncComponent.EventType.OnAfterRenderAsyncAsync,
|
||||
EventAction = () => tcs.Task,
|
||||
}
|
||||
},
|
||||
|
|
@ -3203,7 +3454,7 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
{
|
||||
new NestedAsyncComponent.ExecutionAction
|
||||
{
|
||||
Event = NestedAsyncComponent.EventType.OnAfterRenderAsync,
|
||||
Event = NestedAsyncComponent.EventType.OnAfterRenderAsyncSync,
|
||||
EventAction = () =>
|
||||
{
|
||||
taskCompletionSource.TrySetResult(0);
|
||||
|
|
@ -3291,9 +3542,9 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
|
||||
// All components must be disposed even if some throw as part of being diposed.
|
||||
Assert.True(component.Disposed);
|
||||
Assert.Equal(2, renderer.HandledExceptions.Count);
|
||||
Assert.Contains(exception1, renderer.HandledExceptions);
|
||||
Assert.Contains(exception2, renderer.HandledExceptions);
|
||||
var aex = Assert.IsType<AggregateException>(Assert.Single(renderer.HandledExceptions));
|
||||
Assert.Contains(exception1, aex.InnerExceptions);
|
||||
Assert.Contains(exception2, aex.InnerExceptions);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -3941,6 +4192,7 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
if (TryGetEntry(EventType.OnInit, out var entry))
|
||||
{
|
||||
var result = entry.EventAction();
|
||||
Assert.True(result.IsCompleted, "Task must complete synchronously.");
|
||||
LogResult(result.Result);
|
||||
}
|
||||
}
|
||||
|
|
@ -3949,8 +4201,9 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
{
|
||||
if (TryGetEntry(EventType.OnInitAsyncSync, out var entrySync))
|
||||
{
|
||||
var result = await entrySync.EventAction();
|
||||
LogResult(result);
|
||||
var result = entrySync.EventAction();
|
||||
Assert.True(result.IsCompleted, "Task must complete synchronously.");
|
||||
LogResult(result.Result);
|
||||
}
|
||||
else if (TryGetEntry(EventType.OnInitAsyncAsync, out var entryAsync))
|
||||
{
|
||||
|
|
@ -3964,6 +4217,7 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
if (TryGetEntry(EventType.OnParametersSet, out var entry))
|
||||
{
|
||||
var result = entry.EventAction();
|
||||
Assert.True(result.IsCompleted, "Task must complete synchronously.");
|
||||
LogResult(result.Result);
|
||||
}
|
||||
base.OnParametersSet();
|
||||
|
|
@ -3973,10 +4227,9 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
{
|
||||
if (TryGetEntry(EventType.OnParametersSetAsyncSync, out var entrySync))
|
||||
{
|
||||
var result = await entrySync.EventAction();
|
||||
LogResult(result);
|
||||
|
||||
await entrySync.EventAction();
|
||||
var result = entrySync.EventAction();
|
||||
Assert.True(result.IsCompleted, "Task must complete synchronously.");
|
||||
LogResult(result.Result);
|
||||
}
|
||||
else if (TryGetEntry(EventType.OnParametersSetAsyncAsync, out var entryAsync))
|
||||
{
|
||||
|
|
@ -3993,9 +4246,15 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
|
||||
protected override async Task OnAfterRenderAsync()
|
||||
{
|
||||
if (TryGetEntry(EventType.OnAfterRenderAsync, out var entry))
|
||||
if (TryGetEntry(EventType.OnAfterRenderAsyncSync, out var entrySync))
|
||||
{
|
||||
var result = await entry.EventAction();
|
||||
var result = entrySync.EventAction();
|
||||
Assert.True(result.IsCompleted, "Task must complete synchronously.");
|
||||
LogResult(result.Result);
|
||||
}
|
||||
if (TryGetEntry(EventType.OnAfterRenderAsyncAsync, out var entryAsync))
|
||||
{
|
||||
var result = await entryAsync.EventAction();
|
||||
LogResult(result);
|
||||
}
|
||||
}
|
||||
|
|
@ -4054,7 +4313,8 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
OnParametersSet,
|
||||
OnParametersSetAsyncSync,
|
||||
OnParametersSetAsyncAsync,
|
||||
OnAfterRenderAsync,
|
||||
OnAfterRenderAsyncSync,
|
||||
OnAfterRenderAsyncAsync,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
{
|
||||
try
|
||||
{
|
||||
await Registry.Terminate(circuitId);
|
||||
await Registry.TerminateAsync(circuitId);
|
||||
Log.CircuitTerminatedGracefully(Logger, circuitId);
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||
{
|
||||
// Used to isolate a circuit from a CircuitHost.
|
||||
//
|
||||
// We can't refer to Hub.Items from a CircuitHost - but we want need to be
|
||||
// able to break the link between Hub.Items and a CircuitHost.
|
||||
internal class CircuitHandle
|
||||
{
|
||||
public CircuitHost CircuitHost { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.Web.Rendering;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.JSInterop;
|
||||
|
|
@ -18,9 +19,11 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
{
|
||||
private readonly SemaphoreSlim HandlerLock = new SemaphoreSlim(1);
|
||||
private readonly IServiceScope _scope;
|
||||
private readonly CircuitOptions _options;
|
||||
private readonly CircuitHandler[] _circuitHandlers;
|
||||
private readonly ILogger _logger;
|
||||
private bool _initialized;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the current <see cref="Circuits.Circuit"/>.
|
||||
|
|
@ -42,11 +45,18 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
JSInterop.JSRuntime.SetCurrentJSRuntime(circuitHost.JSRuntime);
|
||||
}
|
||||
|
||||
// This event is fired when there's an unrecoverable exception coming from the circuit, and
|
||||
// it need so be torn down. The registry listens to this even so that the circuit can
|
||||
// be torn down even when a client is not connected.
|
||||
//
|
||||
// We don't expect the registry to do anything with the exception. We only provide it here
|
||||
// for testability.
|
||||
public event UnhandledExceptionEventHandler UnhandledException;
|
||||
|
||||
public CircuitHost(
|
||||
string circuitId,
|
||||
IServiceScope scope,
|
||||
CircuitOptions options,
|
||||
CircuitClientProxy client,
|
||||
RemoteRenderer renderer,
|
||||
IReadOnlyList<ComponentDescriptor> descriptors,
|
||||
|
|
@ -54,23 +64,28 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
CircuitHandler[] circuitHandlers,
|
||||
ILogger logger)
|
||||
{
|
||||
CircuitId = circuitId;
|
||||
CircuitId = circuitId ?? throw new ArgumentNullException(nameof(circuitId));
|
||||
|
||||
_scope = scope ?? throw new ArgumentNullException(nameof(scope));
|
||||
Client = client;
|
||||
Descriptors = descriptors ?? throw new ArgumentNullException(nameof(descriptors));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
Client = client ?? throw new ArgumentNullException(nameof(client));
|
||||
Renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
|
||||
Descriptors = descriptors ?? throw new ArgumentNullException(nameof(descriptors));
|
||||
JSRuntime = jsRuntime ?? throw new ArgumentNullException(nameof(jsRuntime));
|
||||
_logger = logger;
|
||||
_circuitHandlers = circuitHandlers ?? throw new ArgumentNullException(nameof(circuitHandlers));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
Services = scope.ServiceProvider;
|
||||
|
||||
Circuit = new Circuit(this);
|
||||
_circuitHandlers = circuitHandlers;
|
||||
Handle = new CircuitHandle() { CircuitHost = this, };
|
||||
|
||||
Renderer.UnhandledException += Renderer_UnhandledException;
|
||||
Renderer.UnhandledSynchronizationException += SynchronizationContext_UnhandledException;
|
||||
}
|
||||
|
||||
public CircuitHandle Handle { get; }
|
||||
|
||||
public string CircuitId { get; }
|
||||
|
||||
public Circuit Circuit { get; }
|
||||
|
|
@ -85,88 +100,19 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
|
||||
public IServiceProvider Services { get; }
|
||||
|
||||
public void SetCircuitUser(ClaimsPrincipal user)
|
||||
// InitializeAsync is used in a fire-and-forget context, so it's responsible for its own
|
||||
// error handling.
|
||||
public Task InitializeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var authenticationStateProvider = Services.GetService<AuthenticationStateProvider>() as IHostEnvironmentAuthenticationStateProvider;
|
||||
if (authenticationStateProvider != null)
|
||||
Log.InitializationStarted(_logger);
|
||||
|
||||
return Renderer.Dispatcher.InvokeAsync(async () =>
|
||||
{
|
||||
var authenticationState = new AuthenticationState(user);
|
||||
authenticationStateProvider.SetAuthenticationState(Task.FromResult(authenticationState));
|
||||
}
|
||||
}
|
||||
|
||||
internal void SendPendingBatches()
|
||||
{
|
||||
// Dispatch any buffered renders we accumulated during a disconnect.
|
||||
// Note that while the rendering is async, we cannot await it here. The Task returned by ProcessBufferedRenderBatches relies on
|
||||
// OnRenderCompleted to be invoked to complete, and SignalR does not allow concurrent hub method invocations.
|
||||
_ = Renderer.Dispatcher.InvokeAsync(() => Renderer.ProcessBufferedRenderBatches());
|
||||
}
|
||||
|
||||
public async Task EndInvokeJSFromDotNet(long asyncCall, bool succeded, string arguments)
|
||||
{
|
||||
try
|
||||
{
|
||||
AssertInitialized();
|
||||
|
||||
await Renderer.Dispatcher.InvokeAsync(() =>
|
||||
if (_initialized)
|
||||
{
|
||||
SetCurrentCircuitHost(this);
|
||||
if (!succeded)
|
||||
{
|
||||
// We can log the arguments here because it is simply the JS error with the call stack.
|
||||
Log.EndInvokeJSFailed(_logger, asyncCall, arguments);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.EndInvokeJSSucceeded(_logger, asyncCall);
|
||||
}
|
||||
throw new InvalidOperationException("The circuit host is already initialized.");
|
||||
}
|
||||
|
||||
DotNetDispatcher.EndInvoke(arguments);
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.EndInvokeDispatchException(_logger, ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DispatchEvent(string eventDescriptorJson, string eventArgsJson)
|
||||
{
|
||||
WebEventData webEventData;
|
||||
try
|
||||
{
|
||||
AssertInitialized();
|
||||
webEventData = WebEventData.Parse(eventDescriptorJson, eventArgsJson);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.DispatchEventFailedToParseEventData(_logger, ex);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Renderer.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
SetCurrentCircuitHost(this);
|
||||
return Renderer.DispatchEventAsync(
|
||||
webEventData.EventHandlerId,
|
||||
webEventData.EventFieldInfo,
|
||||
webEventData.EventArgs);
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.DispatchEventFailedToDispatchEvent(_logger, webEventData.EventHandlerId.ToString(), ex);
|
||||
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task InitializeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await Renderer.Dispatcher.InvokeAsync(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
SetCurrentCircuitHost(this);
|
||||
|
|
@ -185,92 +131,117 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
var (componentType, domElementSelector) = Descriptors[i];
|
||||
await Renderer.AddComponentAsync(componentType, domElementSelector);
|
||||
}
|
||||
|
||||
Log.InitializationSucceeded(_logger);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// We have to handle all our own errors here, because the upstream caller
|
||||
// has to fire-and-forget this
|
||||
Renderer_UnhandledException(this, ex);
|
||||
// Report errors asynchronously. InitializeAsync is designed not to throw.
|
||||
Log.InitializationFailed(_logger, ex);
|
||||
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async Task BeginInvokeDotNetFromJS(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson)
|
||||
// We handle errors in DisposeAsync because there's no real value in letting it propagate.
|
||||
// We run user code here (CircuitHandlers) and it's reasonable to expect some might throw, however,
|
||||
// there isn't anything better to do than log when one of these exceptions happens - because the
|
||||
// client is already gone.
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
AssertInitialized();
|
||||
Log.DisposeStarted(_logger, CircuitId);
|
||||
|
||||
await Renderer.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
SetCurrentCircuitHost(this);
|
||||
Log.BeginInvokeDotNet(_logger, callId, assemblyName, methodIdentifier, dotNetObjectId);
|
||||
DotNetDispatcher.BeginInvoke(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson);
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
await Renderer.Dispatcher.InvokeAsync(async () =>
|
||||
{
|
||||
// We don't expect any of this code to actually throw, because DotNetDispatcher.BeginInvoke doesn't throw
|
||||
// however, we still want this to get logged if we do.
|
||||
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task OnLocationChangedAsync(string uri, bool intercepted)
|
||||
{
|
||||
try
|
||||
{
|
||||
AssertInitialized();
|
||||
await Renderer.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
SetCurrentCircuitHost(this);
|
||||
Log.LocationChange(_logger, CircuitId, uri);
|
||||
var navigationManager = (RemoteNavigationManager)Services.GetRequiredService<NavigationManager>();
|
||||
navigationManager.NotifyLocationChanged(uri, intercepted);
|
||||
Log.LocationChangeSucceeded(_logger, CircuitId, uri);
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// It's up to the NavigationManager implementation to validate the URI.
|
||||
//
|
||||
// Note that it's also possible that setting the URI could cause a failure in code that listens
|
||||
// to NavigationManager.LocationChanged.
|
||||
//
|
||||
// In either case, a well-behaved client will not send invalid URIs, and we don't really
|
||||
// want to continue processing with the circuit if setting the URI failed inside application
|
||||
// code. The safest thing to do is consider it a critical failure since URI is global state,
|
||||
// and a failure means that an update to global state was partially applied.
|
||||
Log.LocationChangeFailed(_logger, CircuitId, uri, ex);
|
||||
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false));
|
||||
}
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure that no hub or connection can refer to this circuit anymore now that it's shutting down.
|
||||
Handle.CircuitHost = null;
|
||||
_disposed = true;
|
||||
|
||||
try
|
||||
{
|
||||
await OnConnectionDownAsync(CancellationToken.None);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Individual exceptions logged as part of OnConnectionDownAsync - nothing to do here
|
||||
// since we're already shutting down.
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await OnCircuitDownAsync(CancellationToken.None);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Individual exceptions logged as part of OnCircuitDownAsync - nothing to do here
|
||||
// since we're already shutting down.
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Renderer.Dispose();
|
||||
_scope.Dispose();
|
||||
Log.DisposeSucceeded(_logger, CircuitId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.DisposeFailed(_logger, CircuitId, ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Note: we log exceptions and re-throw while running handlers, because there may be multiple
|
||||
// exceptions.
|
||||
private async Task OnCircuitOpenedAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Log.CircuitOpened(_logger, Circuit.Id);
|
||||
|
||||
for (var i = 0; i < _circuitHandlers.Length; i++)
|
||||
await HandlerLock.WaitAsync(cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
var circuitHandler = _circuitHandlers[i];
|
||||
try
|
||||
List<Exception> exceptions = null;
|
||||
|
||||
for (var i = 0; i < _circuitHandlers.Length; i++)
|
||||
{
|
||||
await circuitHandler.OnCircuitOpenedAsync(Circuit, cancellationToken);
|
||||
var circuitHandler = _circuitHandlers[i];
|
||||
try
|
||||
{
|
||||
await circuitHandler.OnCircuitOpenedAsync(Circuit, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.CircuitHandlerFailed(_logger, circuitHandler, nameof(CircuitHandler.OnCircuitOpenedAsync), ex);
|
||||
exceptions ??= new List<Exception>();
|
||||
exceptions.Add(ex);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
if (exceptions != null)
|
||||
{
|
||||
OnHandlerError(circuitHandler, nameof(CircuitHandler.OnCircuitOpenedAsync), ex);
|
||||
throw new AggregateException("Encountered exceptions while executing circuit handlers.", exceptions);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
HandlerLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task OnConnectionUpAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Log.ConnectionUp(_logger, Circuit.Id, Client.ConnectionId);
|
||||
|
||||
await HandlerLock.WaitAsync(cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
await HandlerLock.WaitAsync(cancellationToken);
|
||||
List<Exception> exceptions = null;
|
||||
|
||||
for (var i = 0; i < _circuitHandlers.Length; i++)
|
||||
{
|
||||
|
|
@ -281,9 +252,16 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnHandlerError(circuitHandler, nameof(CircuitHandler.OnConnectionUpAsync), ex);
|
||||
Log.CircuitHandlerFailed(_logger, circuitHandler, nameof(CircuitHandler.OnConnectionUpAsync), ex);
|
||||
exceptions ??= new List<Exception>();
|
||||
exceptions.Add(ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (exceptions != null)
|
||||
{
|
||||
throw new AggregateException("Encountered exceptions while executing circuit handlers.", exceptions);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -295,9 +273,11 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
{
|
||||
Log.ConnectionDown(_logger, Circuit.Id, Client.ConnectionId);
|
||||
|
||||
await HandlerLock.WaitAsync(cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
await HandlerLock.WaitAsync(cancellationToken);
|
||||
List<Exception> exceptions = null;
|
||||
|
||||
for (var i = 0; i < _circuitHandlers.Length; i++)
|
||||
{
|
||||
|
|
@ -308,9 +288,16 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnHandlerError(circuitHandler, nameof(CircuitHandler.OnConnectionDownAsync), ex);
|
||||
Log.CircuitHandlerFailed(_logger, circuitHandler, nameof(CircuitHandler.OnConnectionDownAsync), ex);
|
||||
exceptions ??= new List<Exception>();
|
||||
exceptions.Add(ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (exceptions != null)
|
||||
{
|
||||
throw new AggregateException("Encountered exceptions while executing circuit handlers.", exceptions);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -318,46 +305,229 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
}
|
||||
}
|
||||
|
||||
protected virtual void OnHandlerError(CircuitHandler circuitHandler, string handlerMethod, Exception ex)
|
||||
{
|
||||
Log.UnhandledExceptionInvokingCircuitHandler(_logger, circuitHandler, handlerMethod, ex);
|
||||
}
|
||||
|
||||
private async Task OnCircuitDownAsync()
|
||||
private async Task OnCircuitDownAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Log.CircuitClosed(_logger, Circuit.Id);
|
||||
|
||||
for (var i = 0; i < _circuitHandlers.Length; i++)
|
||||
await HandlerLock.WaitAsync(cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
var circuitHandler = _circuitHandlers[i];
|
||||
try
|
||||
List<Exception> exceptions = null;
|
||||
|
||||
for (var i = 0; i < _circuitHandlers.Length; i++)
|
||||
{
|
||||
await circuitHandler.OnCircuitClosedAsync(Circuit, default);
|
||||
var circuitHandler = _circuitHandlers[i];
|
||||
try
|
||||
{
|
||||
await circuitHandler.OnCircuitClosedAsync(Circuit, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.CircuitHandlerFailed(_logger, circuitHandler, nameof(CircuitHandler.OnCircuitClosedAsync), ex);
|
||||
exceptions ??= new List<Exception>();
|
||||
exceptions.Add(ex);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
if (exceptions != null)
|
||||
{
|
||||
OnHandlerError(circuitHandler, nameof(CircuitHandler.OnCircuitClosedAsync), ex);
|
||||
throw new AggregateException("Encountered exceptions while executing circuit handlers.", exceptions);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
HandlerLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
// BeginInvokeDotNetFromJS is used in a fire-and-forget context, so it's responsible for its own
|
||||
// error handling.
|
||||
public async Task BeginInvokeDotNetFromJS(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson)
|
||||
{
|
||||
Log.DisposingCircuit(_logger, CircuitId);
|
||||
AssertInitialized();
|
||||
AssertNotDisposed();
|
||||
|
||||
await Renderer.Dispatcher.InvokeAsync(async () =>
|
||||
try
|
||||
{
|
||||
try
|
||||
await Renderer.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
await OnConnectionDownAsync(CancellationToken.None);
|
||||
await OnCircuitDownAsync();
|
||||
}
|
||||
finally
|
||||
SetCurrentCircuitHost(this);
|
||||
Log.BeginInvokeDotNet(_logger, callId, assemblyName, methodIdentifier, dotNetObjectId);
|
||||
DotNetDispatcher.BeginInvoke(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson);
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// We don't expect any of this code to actually throw, because DotNetDispatcher.BeginInvoke doesn't throw
|
||||
// however, we still want this to get logged if we do.
|
||||
Log.BeginInvokeDotNetFailed(_logger, callId, assemblyName, methodIdentifier, dotNetObjectId, ex);
|
||||
if (Client.Connected)
|
||||
{
|
||||
Renderer.Dispose();
|
||||
_scope.Dispose();
|
||||
await NotifyClientError(Client, "Interop call failed.");
|
||||
}
|
||||
});
|
||||
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false));
|
||||
}
|
||||
}
|
||||
|
||||
// EndInvokeJSFromDotNet is used in a fire-and-forget context, so it's responsible for its own
|
||||
// error handling.
|
||||
public async Task EndInvokeJSFromDotNet(long asyncCall, bool succeded, string arguments)
|
||||
{
|
||||
AssertInitialized();
|
||||
AssertNotDisposed();
|
||||
|
||||
try
|
||||
{
|
||||
await Renderer.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
SetCurrentCircuitHost(this);
|
||||
if (!succeded)
|
||||
{
|
||||
// We can log the arguments here because it is simply the JS error with the call stack.
|
||||
Log.EndInvokeJSFailed(_logger, asyncCall, arguments);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.EndInvokeJSSucceeded(_logger, asyncCall);
|
||||
}
|
||||
|
||||
DotNetDispatcher.EndInvoke(arguments);
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// An error completing JS interop means that the user sent invalid data, a well-behaved
|
||||
// client won't do this.
|
||||
Log.EndInvokeDispatchException(_logger, ex);
|
||||
if (Client.Connected)
|
||||
{
|
||||
await NotifyClientError(Client, "Invalid interop arguments.");
|
||||
}
|
||||
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false));
|
||||
}
|
||||
}
|
||||
|
||||
// DispatchEvent is used in a fire-and-forget context, so it's responsible for its own
|
||||
// error handling.
|
||||
public async Task DispatchEvent(string eventDescriptorJson, string eventArgsJson)
|
||||
{
|
||||
AssertInitialized();
|
||||
AssertNotDisposed();
|
||||
|
||||
WebEventData webEventData;
|
||||
try
|
||||
{
|
||||
AssertInitialized();
|
||||
webEventData = WebEventData.Parse(eventDescriptorJson, eventArgsJson);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Invalid event data is fatal. We expect a well-behaved client to send valid JSON.
|
||||
Log.DispatchEventFailedToParseEventData(_logger, ex);
|
||||
if (Client.Connected)
|
||||
{
|
||||
await NotifyClientError(Client, "Invalid event data.");
|
||||
}
|
||||
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false));
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Renderer.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
SetCurrentCircuitHost(this);
|
||||
return Renderer.DispatchEventAsync(
|
||||
webEventData.EventHandlerId,
|
||||
webEventData.EventFieldInfo,
|
||||
webEventData.EventArgs);
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// A failure in dispatching an event means that it was an attempt to use an invalid event id.
|
||||
// A well-behaved client won't do this.
|
||||
Log.DispatchEventFailedToDispatchEvent(_logger, webEventData.EventHandlerId.ToString(), ex);
|
||||
if (Client.Connected)
|
||||
{
|
||||
await NotifyClientError(Client, "Failed to dispatch event.");
|
||||
}
|
||||
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false));
|
||||
}
|
||||
}
|
||||
|
||||
// OnLocationChangedAsync is used in a fire-and-forget context, so it's responsible for its own
|
||||
// error handling.
|
||||
public async Task OnLocationChangedAsync(string uri, bool intercepted)
|
||||
{
|
||||
AssertInitialized();
|
||||
AssertNotDisposed();
|
||||
|
||||
try
|
||||
{
|
||||
await Renderer.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
SetCurrentCircuitHost(this);
|
||||
Log.LocationChange(_logger, uri, CircuitId);
|
||||
var navigationManager = (RemoteNavigationManager)Services.GetRequiredService<NavigationManager>();
|
||||
navigationManager.NotifyLocationChanged(uri, intercepted);
|
||||
Log.LocationChangeSucceeded(_logger, uri, CircuitId);
|
||||
});
|
||||
}
|
||||
|
||||
// It's up to the NavigationManager implementation to validate the URI.
|
||||
//
|
||||
// Note that it's also possible that setting the URI could cause a failure in code that listens
|
||||
// to NavigationManager.LocationChanged.
|
||||
//
|
||||
// In either case, a well-behaved client will not send invalid URIs, and we don't really
|
||||
// want to continue processing with the circuit if setting the URI failed inside application
|
||||
// code. The safest thing to do is consider it a critical failure since URI is global state,
|
||||
// and a failure means that an update to global state was partially applied.
|
||||
catch (LocationChangeException nex)
|
||||
{
|
||||
// LocationChangeException means that it failed in user-code. Treat this like an unhandled
|
||||
// exception in user-code.
|
||||
Log.LocationChangeFailedInCircuit(_logger, uri, CircuitId, nex);
|
||||
await ReportUnhandledException(nex);
|
||||
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(nex, isTerminating: false));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Any other exception means that it failed validation, or inside the NavigationManager. Treat
|
||||
// this like bad data.
|
||||
Log.LocationChangeFailed(_logger, uri, CircuitId, ex);
|
||||
if (Client.Connected)
|
||||
{
|
||||
await NotifyClientError(Client, $"Location change to {uri} failed.");
|
||||
}
|
||||
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false));
|
||||
}
|
||||
}
|
||||
|
||||
public void SetCircuitUser(ClaimsPrincipal user)
|
||||
{
|
||||
// This can be called before the circuit is initialized.
|
||||
AssertNotDisposed();
|
||||
|
||||
var authenticationStateProvider = Services.GetService<AuthenticationStateProvider>() as IHostEnvironmentAuthenticationStateProvider;
|
||||
if (authenticationStateProvider != null)
|
||||
{
|
||||
var authenticationState = new AuthenticationState(user);
|
||||
authenticationStateProvider.SetAuthenticationState(Task.FromResult(authenticationState));
|
||||
}
|
||||
}
|
||||
|
||||
public void SendPendingBatches()
|
||||
{
|
||||
AssertInitialized();
|
||||
AssertNotDisposed();
|
||||
|
||||
// Dispatch any buffered renders we accumulated during a disconnect.
|
||||
// Note that while the rendering is async, we cannot await it here. The Task returned by ProcessBufferedRenderBatches relies on
|
||||
// OnRenderCompleted to be invoked to complete, and SignalR does not allow concurrent hub method invocations.
|
||||
_ = Renderer.Dispatcher.InvokeAsync(() => Renderer.ProcessBufferedRenderBatches());
|
||||
}
|
||||
|
||||
private void AssertInitialized()
|
||||
|
|
@ -368,26 +538,86 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
}
|
||||
}
|
||||
|
||||
private void Renderer_UnhandledException(object sender, Exception e)
|
||||
private void AssertNotDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(objectName: null);
|
||||
}
|
||||
}
|
||||
|
||||
// An unhandled exception from the renderer is always fatal because it came from user code.
|
||||
// We want to notify the client if it's still connected, and then tear-down the circuit.
|
||||
private async void Renderer_UnhandledException(object sender, Exception e)
|
||||
{
|
||||
await ReportUnhandledException(e);
|
||||
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(e, isTerminating: false));
|
||||
}
|
||||
|
||||
private void SynchronizationContext_UnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||
// An unhandled exception from the renderer is always fatal because it came from user code.
|
||||
// We want to notify the client if it's still connected, and then tear-down the circuit.
|
||||
private async void SynchronizationContext_UnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
await ReportUnhandledException((Exception)e.ExceptionObject);
|
||||
UnhandledException?.Invoke(this, e);
|
||||
}
|
||||
|
||||
private async Task ReportUnhandledException(Exception exception)
|
||||
{
|
||||
Log.CircuitUnhandledException(_logger, CircuitId, exception);
|
||||
if (!Client.Connected)
|
||||
{
|
||||
_logger.LogDebug("Client is disconnected YO.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_options.DetailedErrors)
|
||||
{
|
||||
await NotifyClientError(Client, exception.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
var message =
|
||||
$"There was an unhandled exception on the current circuit, so this circuit will be terminated. For more details turn on " +
|
||||
$"detailed exceptions in '{typeof(CircuitOptions).Name}.{nameof(CircuitOptions.DetailedErrors)}'";
|
||||
await NotifyClientError(Client, message);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.CircuitUnhandledExceptionFailed(_logger, CircuitId, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task NotifyClientError(IClientProxy client, string error)
|
||||
{
|
||||
_logger.LogDebug("About to notify of an error");
|
||||
await client.SendAsync("JS.Error", error);
|
||||
_logger.LogDebug("Completed notify of an error");
|
||||
}
|
||||
|
||||
private static class Log
|
||||
{
|
||||
private static readonly Action<ILogger, Type, string, string, Exception> _unhandledExceptionInvokingCircuitHandler;
|
||||
private static readonly Action<ILogger, string, Exception> _disposingCircuit;
|
||||
private static readonly Action<ILogger, Exception> _intializationStarted;
|
||||
private static readonly Action<ILogger, Exception> _intializationSucceded;
|
||||
private static readonly Action<ILogger, Exception> _intializationFailed;
|
||||
private static readonly Action<ILogger, string, Exception> _disposeStarted;
|
||||
private static readonly Action<ILogger, string, Exception> _disposeSucceded;
|
||||
private static readonly Action<ILogger, string, Exception> _disposeFailed;
|
||||
private static readonly Action<ILogger, string, Exception> _onCircuitOpened;
|
||||
private static readonly Action<ILogger, string, string, Exception> _onConnectionUp;
|
||||
private static readonly Action<ILogger, string, string, Exception> _onConnectionDown;
|
||||
private static readonly Action<ILogger, string, Exception> _onCircuitClosed;
|
||||
private static readonly Action<ILogger, Type, string, string, Exception> _circuitHandlerFailed;
|
||||
private static readonly Action<ILogger, string, Exception> _circuitUnhandledException;
|
||||
private static readonly Action<ILogger, string, Exception> _circuitUnhandledExceptionFailed;
|
||||
|
||||
private static readonly Action<ILogger, string, string, string, Exception> _beginInvokeDotNetStatic;
|
||||
private static readonly Action<ILogger, string, long, string, Exception> _beginInvokeDotNetInstance;
|
||||
private static readonly Action<ILogger, string, string, string, Exception> _beginInvokeDotNetStaticFailed;
|
||||
private static readonly Action<ILogger, string, long, string, Exception> _beginInvokeDotNetInstanceFailed;
|
||||
private static readonly Action<ILogger, Exception> _endInvokeDispatchException;
|
||||
private static readonly Action<ILogger, long, string, Exception> _endInvokeJSFailed;
|
||||
private static readonly Action<ILogger, long, Exception> _endInvokeJSSucceeded;
|
||||
|
|
@ -396,39 +626,71 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
private static readonly Action<ILogger, string, string, Exception> _locationChange;
|
||||
private static readonly Action<ILogger, string, string, Exception> _locationChangeSucceeded;
|
||||
private static readonly Action<ILogger, string, string, Exception> _locationChangeFailed;
|
||||
private static readonly Action<ILogger, string, string, Exception> _locationChangeFailedInCircuit;
|
||||
|
||||
private static class EventIds
|
||||
{
|
||||
public static readonly EventId ExceptionInvokingCircuitHandlerMethod = new EventId(100, "ExceptionInvokingCircuitHandlerMethod");
|
||||
public static readonly EventId DisposingCircuit = new EventId(101, "DisposingCircuitHost");
|
||||
public static readonly EventId OnCircuitOpened = new EventId(102, "OnCircuitOpened");
|
||||
public static readonly EventId OnConnectionUp = new EventId(103, "OnConnectionUp");
|
||||
public static readonly EventId OnConnectionDown = new EventId(104, "OnConnectionDown");
|
||||
public static readonly EventId OnCircuitClosed = new EventId(105, "OnCircuitClosed");
|
||||
public static readonly EventId InvalidBrowserEventFormat = new EventId(106, "InvalidBrowserEventFormat");
|
||||
public static readonly EventId DispatchEventFailedToParseEventData = new EventId(107, "DispatchEventFailedToParseEventData");
|
||||
public static readonly EventId DispatchEventFailedToDispatchEvent = new EventId(108, "DispatchEventFailedToDispatchEvent");
|
||||
public static readonly EventId BeginInvokeDotNet = new EventId(109, "BeginInvokeDotNet");
|
||||
public static readonly EventId EndInvokeDispatchException = new EventId(110, "EndInvokeDispatchException");
|
||||
public static readonly EventId EndInvokeJSFailed = new EventId(111, "EndInvokeJSFailed");
|
||||
public static readonly EventId EndInvokeJSSucceeded = new EventId(112, "EndInvokeJSSucceeded");
|
||||
public static readonly EventId DispatchEventThroughJSInterop = new EventId(113, "DispatchEventThroughJSInterop");
|
||||
public static readonly EventId LocationChange = new EventId(114, "LocationChange");
|
||||
public static readonly EventId LocationChangeSucceded = new EventId(115, "LocationChangeSucceeded");
|
||||
public static readonly EventId LocationChangeFailed = new EventId(116, "LocationChangeFailed");
|
||||
// 100s used for lifecycle stuff
|
||||
public static readonly EventId InitializationStarted = new EventId(100, "InitializationStarted");
|
||||
public static readonly EventId InitializationSucceeded = new EventId(101, "InitializationSucceeded");
|
||||
public static readonly EventId InitializationFailed = new EventId(102, "InitializationFailed");
|
||||
public static readonly EventId DisposeStarted = new EventId(103, "DisposeStarted");
|
||||
public static readonly EventId DisposeSucceeded = new EventId(104, "DisposeSucceeded");
|
||||
public static readonly EventId DisposeFailed = new EventId(105, "DisposeFailed");
|
||||
public static readonly EventId OnCircuitOpened = new EventId(106, "OnCircuitOpened");
|
||||
public static readonly EventId OnConnectionUp = new EventId(107, "OnConnectionUp");
|
||||
public static readonly EventId OnConnectionDown = new EventId(108, "OnConnectionDown");
|
||||
public static readonly EventId OnCircuitClosed = new EventId(109, "OnCircuitClosed");
|
||||
public static readonly EventId CircuitHandlerFailed = new EventId(110, "CircuitHandlerFailed");
|
||||
public static readonly EventId CircuitUnhandledException = new EventId(111, "CircuitUnhandledException");
|
||||
public static readonly EventId CircuitUnhandledExceptionFailed = new EventId(112, "CircuitUnhandledExceptionFailed");
|
||||
|
||||
// 200s used for interactive stuff
|
||||
public static readonly EventId DispatchEventFailedToParseEventData = new EventId(200, "DispatchEventFailedToParseEventData");
|
||||
public static readonly EventId DispatchEventFailedToDispatchEvent = new EventId(201, "DispatchEventFailedToDispatchEvent");
|
||||
public static readonly EventId BeginInvokeDotNet = new EventId(202, "BeginInvokeDotNet");
|
||||
public static readonly EventId BeginInvokeDotNetFailed = new EventId(203, "BeginInvokeDotNetFailed");
|
||||
public static readonly EventId EndInvokeDispatchException = new EventId(204, "EndInvokeDispatchException");
|
||||
public static readonly EventId EndInvokeJSFailed = new EventId(205, "EndInvokeJSFailed");
|
||||
public static readonly EventId EndInvokeJSSucceeded = new EventId(206, "EndInvokeJSSucceeded");
|
||||
public static readonly EventId DispatchEventThroughJSInterop = new EventId(207, "DispatchEventThroughJSInterop");
|
||||
public static readonly EventId LocationChange = new EventId(208, "LocationChange");
|
||||
public static readonly EventId LocationChangeSucceded = new EventId(209, "LocationChangeSucceeded");
|
||||
public static readonly EventId LocationChangeFailed = new EventId(210, "LocationChangeFailed");
|
||||
public static readonly EventId LocationChangeFailedInCircuit = new EventId(211, "LocationChangeFailedInCircuit");
|
||||
}
|
||||
|
||||
static Log()
|
||||
{
|
||||
_unhandledExceptionInvokingCircuitHandler = LoggerMessage.Define<Type, string, string>(
|
||||
LogLevel.Error,
|
||||
EventIds.ExceptionInvokingCircuitHandlerMethod,
|
||||
"Unhandled error invoking circuit handler type {handlerType}.{handlerMethod}: {Message}");
|
||||
|
||||
_disposingCircuit = LoggerMessage.Define<string>(
|
||||
_intializationStarted = LoggerMessage.Define(
|
||||
LogLevel.Debug,
|
||||
EventIds.DisposingCircuit,
|
||||
"Disposing circuit with identifier {CircuitId}");
|
||||
EventIds.InitializationFailed,
|
||||
"Circuit initialization started");
|
||||
|
||||
_intializationSucceded = LoggerMessage.Define(
|
||||
LogLevel.Debug,
|
||||
EventIds.InitializationFailed,
|
||||
"Circuit initialization succeeded");
|
||||
|
||||
_intializationFailed = LoggerMessage.Define(
|
||||
LogLevel.Debug,
|
||||
EventIds.InitializationFailed,
|
||||
"Circuit initialization failed");
|
||||
|
||||
_disposeStarted = LoggerMessage.Define<string>(
|
||||
LogLevel.Debug,
|
||||
EventIds.DisposeStarted,
|
||||
"Disposing circuit {CircuitId} started");
|
||||
|
||||
_disposeSucceded = LoggerMessage.Define<string>(
|
||||
LogLevel.Debug,
|
||||
EventIds.DisposeSucceeded,
|
||||
"Disposing circuit {CircuitId} succeded");
|
||||
|
||||
_disposeFailed = LoggerMessage.Define<string>(
|
||||
LogLevel.Debug,
|
||||
EventIds.DisposeFailed,
|
||||
"Disposing circuit {CircuitId} failed");
|
||||
|
||||
_onCircuitOpened = LoggerMessage.Define<string>(
|
||||
LogLevel.Debug,
|
||||
|
|
@ -450,6 +712,21 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
EventIds.OnCircuitClosed,
|
||||
"Closing circuit with id {CircuitId}.");
|
||||
|
||||
_circuitHandlerFailed = LoggerMessage.Define<Type, string, string>(
|
||||
LogLevel.Error,
|
||||
EventIds.CircuitHandlerFailed,
|
||||
"Unhandled error invoking circuit handler type {handlerType}.{handlerMethod}: {Message}");
|
||||
|
||||
_circuitUnhandledException = LoggerMessage.Define<string>(
|
||||
LogLevel.Error,
|
||||
EventIds.CircuitUnhandledException,
|
||||
"Unhandled exception in circuit {CircuitId}");
|
||||
|
||||
_circuitUnhandledExceptionFailed = LoggerMessage.Define<string>(
|
||||
LogLevel.Debug,
|
||||
EventIds.CircuitUnhandledExceptionFailed,
|
||||
"Failed to transmit exception to client in circuit {CircuitId}");
|
||||
|
||||
_beginInvokeDotNetStatic = LoggerMessage.Define<string, string, string>(
|
||||
LogLevel.Debug,
|
||||
EventIds.BeginInvokeDotNet,
|
||||
|
|
@ -460,6 +737,16 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
EventIds.BeginInvokeDotNet,
|
||||
"Invoking instance method '{MethodIdentifier}' on instance '{DotNetObjectId}' with callback id '{CallId}'");
|
||||
|
||||
_beginInvokeDotNetStaticFailed = LoggerMessage.Define<string, string, string>(
|
||||
LogLevel.Debug,
|
||||
EventIds.BeginInvokeDotNetFailed,
|
||||
"Failed to invoke static method with identifier '{MethodIdentifier}' on assembly '{Assembly}' with callback id '{CallId}'");
|
||||
|
||||
_beginInvokeDotNetInstanceFailed = LoggerMessage.Define<string, long, string>(
|
||||
LogLevel.Debug,
|
||||
EventIds.BeginInvokeDotNetFailed,
|
||||
"Failed to invoke instance method '{MethodIdentifier}' on instance '{DotNetObjectId}' with callback id '{CallId}'");
|
||||
|
||||
_endInvokeDispatchException = LoggerMessage.Define(
|
||||
LogLevel.Debug,
|
||||
EventIds.EndInvokeDispatchException,
|
||||
|
|
@ -488,22 +775,38 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
_locationChange = LoggerMessage.Define<string, string>(
|
||||
LogLevel.Debug,
|
||||
EventIds.LocationChange,
|
||||
"Location changing to {URI} in {CircuitId}.");
|
||||
"Location changing to {URI} in circuit {CircuitId}.");
|
||||
|
||||
_locationChangeSucceeded = LoggerMessage.Define<string, string>(
|
||||
LogLevel.Debug,
|
||||
EventIds.LocationChangeSucceded,
|
||||
"Location change to {URI} in {CircuitId} succeded.");
|
||||
"Location change to {URI} in circuit {CircuitId} succeded.");
|
||||
|
||||
_locationChangeFailed = LoggerMessage.Define<string, string>(
|
||||
LogLevel.Debug,
|
||||
EventIds.LocationChangeFailed,
|
||||
"Location change to {URI} in {CircuitId} failed.");
|
||||
"Location change to {URI} in circuit {CircuitId} failed.");
|
||||
|
||||
_locationChangeFailedInCircuit = LoggerMessage.Define<string, string>(
|
||||
LogLevel.Error,
|
||||
EventIds.LocationChangeFailed,
|
||||
"Location change to {URI} in circuit {CircuitId} failed.");
|
||||
}
|
||||
|
||||
public static void UnhandledExceptionInvokingCircuitHandler(ILogger logger, CircuitHandler handler, string handlerMethod, Exception exception)
|
||||
public static void InitializationStarted(ILogger logger) =>_intializationStarted(logger, null);
|
||||
public static void InitializationSucceeded(ILogger logger) => _intializationSucceded(logger, null);
|
||||
public static void InitializationFailed(ILogger logger, Exception exception) => _intializationFailed(logger, exception);
|
||||
public static void DisposeStarted(ILogger logger, string circuitId) => _disposeStarted(logger, circuitId, null);
|
||||
public static void DisposeSucceeded(ILogger logger, string circuitId) => _disposeSucceded(logger, circuitId, null);
|
||||
public static void DisposeFailed(ILogger logger, string circuitId, Exception exception) => _disposeFailed(logger, circuitId, exception);
|
||||
public static void CircuitOpened(ILogger logger, string circuitId) => _onCircuitOpened(logger, circuitId, null);
|
||||
public static void ConnectionUp(ILogger logger, string circuitId, string connectionId) => _onConnectionUp(logger, circuitId, connectionId, null);
|
||||
public static void ConnectionDown(ILogger logger, string circuitId, string connectionId) => _onConnectionDown(logger, circuitId, connectionId, null);
|
||||
public static void CircuitClosed(ILogger logger, string circuitId) => _onCircuitClosed(logger, circuitId, null);
|
||||
|
||||
public static void CircuitHandlerFailed(ILogger logger, CircuitHandler handler, string handlerMethod, Exception exception)
|
||||
{
|
||||
_unhandledExceptionInvokingCircuitHandler(
|
||||
_circuitHandlerFailed(
|
||||
logger,
|
||||
handler.GetType(),
|
||||
handlerMethod,
|
||||
|
|
@ -511,27 +814,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
exception);
|
||||
}
|
||||
|
||||
public static void DisposingCircuit(ILogger logger, string circuitId) => _disposingCircuit(logger, circuitId, null);
|
||||
|
||||
public static void CircuitOpened(ILogger logger, string circuitId) => _onCircuitOpened(logger, circuitId, null);
|
||||
|
||||
public static void ConnectionUp(ILogger logger, string circuitId, string connectionId) =>
|
||||
_onConnectionUp(logger, circuitId, connectionId, null);
|
||||
|
||||
public static void ConnectionDown(ILogger logger, string circuitId, string connectionId) =>
|
||||
_onConnectionDown(logger, circuitId, connectionId, null);
|
||||
|
||||
public static void CircuitClosed(ILogger logger, string circuitId) =>
|
||||
_onCircuitClosed(logger, circuitId, null);
|
||||
|
||||
public static void CircuitUnhandledException(ILogger logger, string circuitId, Exception exception) => _circuitUnhandledException(logger, circuitId, exception);
|
||||
public static void CircuitUnhandledExceptionFailed(ILogger logger, string circuitId, Exception exception) => _circuitUnhandledExceptionFailed(logger, circuitId, exception);
|
||||
public static void EndInvokeDispatchException(ILogger logger, Exception ex) => _endInvokeDispatchException(logger, ex);
|
||||
|
||||
public static void EndInvokeJSFailed(ILogger logger, long asyncHandle, string arguments) => _endInvokeJSFailed(logger, asyncHandle, arguments, null);
|
||||
|
||||
public static void EndInvokeJSSucceeded(ILogger logger, long asyncCall) => _endInvokeJSSucceeded(logger, asyncCall, null);
|
||||
|
||||
public static void DispatchEventFailedToParseEventData(ILogger logger, Exception ex) => _dispatchEventFailedToParseEventData(logger, ex);
|
||||
|
||||
public static void DispatchEventFailedToDispatchEvent(ILogger logger, string eventHandlerId, Exception ex) => _dispatchEventFailedToDispatchEvent(logger, eventHandlerId ?? "", ex);
|
||||
|
||||
public static void BeginInvokeDotNet(ILogger logger, string callId, string assemblyName, string methodIdentifier, long dotNetObjectId)
|
||||
|
|
@ -546,11 +834,22 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
}
|
||||
}
|
||||
|
||||
public static void LocationChange(ILogger logger, string circuitId, string uri) => _locationChange(logger, circuitId, uri, null);
|
||||
public static void BeginInvokeDotNetFailed(ILogger logger, string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, Exception exception)
|
||||
{
|
||||
if (assemblyName != null)
|
||||
{
|
||||
_beginInvokeDotNetStaticFailed(logger, methodIdentifier, assemblyName, callId, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
_beginInvokeDotNetInstanceFailed(logger, methodIdentifier, dotNetObjectId, callId, null);
|
||||
}
|
||||
}
|
||||
|
||||
public static void LocationChangeSucceeded(ILogger logger, string circuitId, string uri) => _locationChangeSucceeded(logger, circuitId, uri, null);
|
||||
|
||||
public static void LocationChangeFailed(ILogger logger, string circuitId, string uri, Exception exception) => _locationChangeFailed(logger, circuitId, uri, exception);
|
||||
public static void LocationChange(ILogger logger, string uri, string circuitId) => _locationChange(logger, uri, circuitId, null);
|
||||
public static void LocationChangeSucceeded(ILogger logger, string uri, string circuitId) => _locationChangeSucceeded(logger, uri, circuitId, null);
|
||||
public static void LocationChangeFailed(ILogger logger, string uri, string circuitId, Exception exception) => _locationChangeFailed(logger, uri, circuitId, exception);
|
||||
public static void LocationChangeFailedInCircuit(ILogger logger, string uri, string circuitId, Exception exception) => _locationChangeFailedInCircuit(logger, uri, circuitId, exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,10 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
// This will likely never happen, except perhaps in unit tests, since CircuitIds are unique.
|
||||
throw new ArgumentException($"Circuit with identity {circuitHost.CircuitId} is already registered.");
|
||||
}
|
||||
|
||||
// Register for unhandled exceptions from the circuit. The registry is responsible for tearing
|
||||
// down the circuit on errors.
|
||||
circuitHost.UnhandledException += CircuitHost_UnhandledException;
|
||||
}
|
||||
|
||||
public virtual Task DisconnectAsync(CircuitHost circuitHost, string connectionId)
|
||||
|
|
@ -154,6 +158,16 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
DisconnectedCircuits.Set(circuitHost.CircuitId, entry, entryOptions);
|
||||
}
|
||||
|
||||
// ConnectAsync is called from the CircuitHub - but the error handling story is a little bit complicated.
|
||||
// We return the circuit from this method, but need to clean up the circuit on failure. So we don't want to
|
||||
// throw from this method because we don't want to return a *failed* circuit.
|
||||
//
|
||||
// The solution is to handle exceptions here, and then return null to represent failure.
|
||||
//
|
||||
// 1. If the circuit id is invalue return null
|
||||
// 2. If the circuit is not found return null
|
||||
// 3. If the circuit is found, but fails to connect, we need to dispose it here and return null
|
||||
// 4. If everything goes well, return the circuit.
|
||||
public virtual async Task<CircuitHost> ConnectAsync(string circuitId, IClientProxy clientProxy, string connectionId, CancellationToken cancellationToken)
|
||||
{
|
||||
Log.CircuitConnectStarted(_logger, circuitId);
|
||||
|
|
@ -169,6 +183,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
|
||||
Task circuitHandlerTask;
|
||||
|
||||
// We don't expect any of the logic inside the lock to throw, or run user code.
|
||||
lock (CircuitRegistryLock)
|
||||
{
|
||||
// Transition the host from disconnected to connected if it's available. In this critical section, we return
|
||||
|
|
@ -188,7 +203,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
// b) out of order connection-up \ connection-down events e.g. a client that disconnects as soon it finishes reconnecting.
|
||||
|
||||
// Dispatch the circuit handlers inside the sync context to ensure the order of execution. CircuitHost executes circuit handlers inside of
|
||||
//
|
||||
// the sync context.
|
||||
circuitHandlerTask = circuitHost.Renderer.Dispatcher.InvokeAsync(async () =>
|
||||
{
|
||||
if (previouslyConnected)
|
||||
|
|
@ -200,13 +215,22 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
|
||||
await circuitHost.OnConnectionUpAsync(cancellationToken);
|
||||
});
|
||||
|
||||
Log.ReconnectionSucceeded(_logger, circuitId);
|
||||
}
|
||||
|
||||
await circuitHandlerTask;
|
||||
try
|
||||
{
|
||||
await circuitHandlerTask;
|
||||
Log.ReconnectionSucceeded(_logger, circuitId);
|
||||
return circuitHost;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.FailedToReconnectToCircuit(_logger, circuitId, ex);
|
||||
await TerminateAsync(circuitId);
|
||||
|
||||
return circuitHost;
|
||||
// Return null on failure, because we need to clean up the circuit.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual (CircuitHost circuitHost, bool previouslyConnected) ConnectCore(string circuitId, IClientProxy clientProxy, string connectionId)
|
||||
|
|
@ -268,6 +292,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
|
||||
try
|
||||
{
|
||||
entry.CircuitHost.UnhandledException -= CircuitHost_UnhandledException;
|
||||
await entry.CircuitHost.DisposeAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -288,7 +313,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
}
|
||||
}
|
||||
|
||||
public ValueTask Terminate(string circuitId)
|
||||
public ValueTask TerminateAsync(string circuitId)
|
||||
{
|
||||
CircuitHost circuitHost;
|
||||
DisconnectedCircuitEntry entry = default;
|
||||
|
|
@ -302,13 +327,33 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
Log.CircuitDisconnectedPermanently(_logger, circuitHost.CircuitId);
|
||||
circuitHost.Client.SetDisconnected();
|
||||
}
|
||||
else
|
||||
{
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
return circuitHost?.DisposeAsync() ?? default;
|
||||
if (circuitHost != null)
|
||||
{
|
||||
circuitHost.UnhandledException -= CircuitHost_UnhandledException;
|
||||
return circuitHost.DisposeAsync();
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
// We don't need to do anything with the exception here, logging and sending exceptions to the client
|
||||
// is done inside the circuit host.
|
||||
private async void CircuitHost_UnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
var circuitHost = (CircuitHost)sender;
|
||||
|
||||
try
|
||||
{
|
||||
// This will dispose the circuit and remove it from the registry.
|
||||
await TerminateAsync(circuitHost.CircuitId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// We don't expect TerminateAsync to throw, but we want exceptions here for completeness.
|
||||
Log.CircuitExceptionHandlerFailed(_logger, circuitHost.CircuitId, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct DisconnectedCircuitEntry
|
||||
|
|
@ -339,6 +384,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
private static readonly Action<ILogger, string, Exception> _circuitMarkedDisconnected;
|
||||
private static readonly Action<ILogger, string, Exception> _circuitDisconnectedPermanently;
|
||||
private static readonly Action<ILogger, string, EvictionReason, Exception> _circuitEvicted;
|
||||
private static readonly Action<ILogger, string, Exception> _circuitExceptionHandlerFailed;
|
||||
|
||||
private static class EventIds
|
||||
{
|
||||
|
|
@ -355,6 +401,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
public static readonly EventId CircuitMarkedDisconnected = new EventId(110, "CircuitMarkedDisconnected");
|
||||
public static readonly EventId CircuitEvicted = new EventId(111, "CircuitEvicted");
|
||||
public static readonly EventId CircuitDisconnectedPermanently = new EventId(112, "CircuitDisconnectedPermanently");
|
||||
public static readonly EventId CircuitExceptionHandlerFailed = new EventId(113, "CircuitExceptionHandlerFailed");
|
||||
}
|
||||
|
||||
static Log()
|
||||
|
|
@ -428,6 +475,11 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
LogLevel.Debug,
|
||||
EventIds.CircuitEvicted,
|
||||
"Circuit with id {CircuitId} evicted due to {EvictionReason}.");
|
||||
|
||||
_circuitExceptionHandlerFailed = LoggerMessage.Define<string>(
|
||||
LogLevel.Error,
|
||||
EventIds.CircuitExceptionHandlerFailed,
|
||||
"Exception handler for {CircuitId} failed.");
|
||||
}
|
||||
|
||||
public static void UnhandledExceptionDisposingCircuitHost(ILogger logger, Exception exception) =>
|
||||
|
|
@ -448,8 +500,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
public static void ConnectingToDisconnectedCircuit(ILogger logger, string circuitId, string connectionId) =>
|
||||
_connectingToDisconnectedCircuit(logger, circuitId, connectionId, null);
|
||||
|
||||
public static void FailedToReconnectToCircuit(ILogger logger, string circuitId) =>
|
||||
_failedToReconnectToCircuit(logger, circuitId, null);
|
||||
public static void FailedToReconnectToCircuit(ILogger logger, string circuitId, Exception exception = null) =>
|
||||
_failedToReconnectToCircuit(logger, circuitId, exception);
|
||||
|
||||
public static void ReconnectionSucceeded(ILogger logger, string circuitId) =>
|
||||
_reconnectionSucceeded(logger, circuitId, null);
|
||||
|
|
@ -471,6 +523,9 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
|
||||
public static void CircuitEvicted(ILogger logger, string circuitId, EvictionReason evictionReason) =>
|
||||
_circuitEvicted(logger, circuitId, evictionReason, null);
|
||||
|
||||
public static void CircuitExceptionHandlerFailed(ILogger logger, string circuitId, Exception exception) =>
|
||||
_circuitExceptionHandlerFailed(logger, circuitId, exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,9 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.Web.Rendering;
|
||||
using Microsoft.AspNetCore.Components.Routing;
|
||||
using Microsoft.AspNetCore.Components.Web.Rendering;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
|
@ -23,9 +20,9 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly ILogger _logger;
|
||||
private readonly CircuitIdFactory _circuitIdFactory;
|
||||
private readonly CircuitOptions _options;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public DefaultCircuitFactory(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
|
|
@ -34,10 +31,11 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
IOptions<CircuitOptions> options)
|
||||
{
|
||||
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
|
||||
_loggerFactory = loggerFactory;
|
||||
_logger = _loggerFactory.CreateLogger<CircuitFactory>();
|
||||
_loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
|
||||
_circuitIdFactory = circuitIdFactory ?? throw new ArgumentNullException(nameof(circuitIdFactory));
|
||||
_options = options.Value;
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
|
||||
_logger = _loggerFactory.CreateLogger<DefaultCircuitFactory>();
|
||||
}
|
||||
|
||||
public override CircuitHost CreateCircuitHost(
|
||||
|
|
@ -49,7 +47,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
{
|
||||
// We do as much intialization as possible eagerly in this method, which makes the error handling
|
||||
// story much simpler. If we throw from here, it's handled inside the initial hub method.
|
||||
var components = ResolveComponentMetadata(httpContext, client);
|
||||
var components = ResolveComponentMetadata(httpContext);
|
||||
|
||||
var scope = _scopeFactory.CreateScope();
|
||||
var encoder = scope.ServiceProvider.GetRequiredService<HtmlEncoder>();
|
||||
|
|
@ -88,6 +86,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
var circuitHost = new CircuitHost(
|
||||
_circuitIdFactory.CreateCircuitId(),
|
||||
scope,
|
||||
_options,
|
||||
client,
|
||||
renderer,
|
||||
components,
|
||||
|
|
@ -103,28 +102,17 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
return circuitHost;
|
||||
}
|
||||
|
||||
internal static List<ComponentDescriptor> ResolveComponentMetadata(HttpContext httpContext, CircuitClientProxy client)
|
||||
public static IReadOnlyList<ComponentDescriptor> ResolveComponentMetadata(HttpContext httpContext)
|
||||
{
|
||||
if (!client.Connected)
|
||||
var endpoint = httpContext.GetEndpoint();
|
||||
if (endpoint == null)
|
||||
{
|
||||
// This is the prerendering case. Descriptors will be registered by the prerenderer.
|
||||
return new List<ComponentDescriptor>();
|
||||
throw new InvalidOperationException(
|
||||
$"{nameof(ComponentHub)} doesn't have an associated endpoint. " +
|
||||
"Use 'app.UseEndpoints(endpoints => endpoints.MapBlazorHub<App>(\"app\"))' to register your hub.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var endpointFeature = httpContext.Features.Get<IEndpointFeature>();
|
||||
var endpoint = endpointFeature?.Endpoint;
|
||||
if (endpoint == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{nameof(ComponentHub)} doesn't have an associated endpoint. " +
|
||||
"Use 'app.UseEndpoints(endpoints => endpoints.MapBlazorHub<App>(\"app\"))' to register your hub.");
|
||||
}
|
||||
|
||||
var componentsMetadata = endpoint.Metadata.OfType<ComponentDescriptor>().ToList();
|
||||
|
||||
return componentsMetadata;
|
||||
}
|
||||
return endpoint.Metadata.GetOrderedMetadata<ComponentDescriptor>();
|
||||
}
|
||||
|
||||
private static class Log
|
||||
|
|
|
|||
|
|
@ -7,15 +7,35 @@ using System.Threading.Tasks;
|
|||
using Microsoft.AspNetCore.Components.Server.Circuits;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// A SignalR hub that accepts connections to an ASP.NET Core Components application.
|
||||
/// </summary>
|
||||
// Some notes about our expectations for error handling:
|
||||
//
|
||||
// In general, we need to prevent any client from interacting with a circuit that's in an unpredictable
|
||||
// state. This means that when a circuit throws an unhandled exception our top priority is to
|
||||
// unregister and dispose the circuit. This will prevent any new dispatches from the client
|
||||
// from making it into application code.
|
||||
//
|
||||
// As part of this process, we also notify the client (if there is one) of the error, and we
|
||||
// *expect* a well-behaved client to disconnect. A malicious client can't be expected to disconnect,
|
||||
// but since we've unregistered the circuit they won't be able to access it anyway. When a call
|
||||
// comes into any hub method and the circuit has been disassociated, we will abort the connection.
|
||||
// It's safe to assume that's the result of a race condition or misbehaving client.
|
||||
//
|
||||
// Now it's important to remember that we can only abort a connection as part of a hub method call.
|
||||
// We can dispose a circuit in the background, but we have to deal with a possible race condition
|
||||
// any time we try to acquire access to the circuit - because it could have gone away in the
|
||||
// background - outside of the scope of a hub method.
|
||||
//
|
||||
// In general we author our Hub methods as async methods, but we fire-and-forget anything that
|
||||
// needs access to the circuit/application state to unblock the message loop. Using async in our
|
||||
// Hub methods allows us to ensure message delivery to the client before we abort the connection
|
||||
// in error cases.
|
||||
internal sealed class ComponentHub : Hub
|
||||
{
|
||||
private static readonly object CircuitKey = new object();
|
||||
|
|
@ -24,10 +44,6 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
private readonly CircuitOptions _options;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Intended for framework use only. Applications should not instantiate
|
||||
/// this class directly.
|
||||
/// </summary>
|
||||
public ComponentHub(
|
||||
CircuitFactory circuitFactory,
|
||||
CircuitRegistry circuitRegistry,
|
||||
|
|
@ -45,24 +61,11 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
/// </summary>
|
||||
public static PathString DefaultPath { get; } = "/_blazor";
|
||||
|
||||
/// <summary>
|
||||
/// For unit testing only.
|
||||
/// </summary>
|
||||
// We store the circuit host in Context.Items which is tied to the lifetime of the underlying
|
||||
// SignalR connection. There's no need to clean this up, it's a non-owning reference and it
|
||||
// will go away when the connection does.
|
||||
internal CircuitHost CircuitHost
|
||||
{
|
||||
get => (CircuitHost)Context.Items[CircuitKey];
|
||||
private set => Context.Items[CircuitKey] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Intended for framework use only. Applications should not call this method directly.
|
||||
/// </summary>
|
||||
public override Task OnDisconnectedAsync(Exception exception)
|
||||
{
|
||||
var circuitHost = CircuitHost;
|
||||
// If the CircuitHost is gone now this isn't an error. This could happen if the disconnect
|
||||
// if the result of well behaving client hanging up after an unhandled exception.
|
||||
var circuitHost = GetCircuit();
|
||||
if (circuitHost == null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
|
|
@ -71,15 +74,16 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
return _circuitRegistry.DisconnectAsync(circuitHost, Context.ConnectionId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Intended for framework use only. Applications should not call this method directly.
|
||||
/// </summary>
|
||||
public string StartCircuit(string baseUri, string uri)
|
||||
public async ValueTask<string> StartCircuit(string baseUri, string uri)
|
||||
{
|
||||
if (CircuitHost != null)
|
||||
var circuitHost = GetCircuit();
|
||||
if (circuitHost != null)
|
||||
{
|
||||
Log.CircuitAlreadyInitialized(_logger, CircuitHost.CircuitId);
|
||||
NotifyClientError(Clients.Caller, $"The circuit host '{CircuitHost.CircuitId}' has already been initialized.");
|
||||
// This is an error condition and an attempt to bind multiple circuits to a single connection.
|
||||
// We can reject this and terminate the connection.
|
||||
Log.CircuitAlreadyInitialized(_logger, circuitHost.CircuitId);
|
||||
await NotifyClientError(Clients.Caller, $"The circuit host '{circuitHost.CircuitId}' has already been initialized.");
|
||||
Context.Abort();
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -90,34 +94,33 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
{
|
||||
// We do some really minimal validation here to prevent obviously wrong data from getting in
|
||||
// without duplicating too much logic.
|
||||
//
|
||||
// This is an error condition attempting to initialize the circuit in a way that would fail.
|
||||
// We can reject this and terminate the connection.
|
||||
Log.InvalidInputData(_logger);
|
||||
_ = NotifyClientError(Clients.Caller, $"The uris provided are invalid.");
|
||||
await NotifyClientError(Clients.Caller, $"The uris provided are invalid.");
|
||||
Context.Abort();
|
||||
return null;
|
||||
}
|
||||
|
||||
var circuitClient = new CircuitClientProxy(Clients.Caller, Context.ConnectionId);
|
||||
if (DefaultCircuitFactory.ResolveComponentMetadata(Context.GetHttpContext(), circuitClient).Count == 0)
|
||||
// From this point, we can try to actually initialize the circuit.
|
||||
if (DefaultCircuitFactory.ResolveComponentMetadata(Context.GetHttpContext()).Count == 0)
|
||||
{
|
||||
var endpointFeature = Context.GetHttpContext().Features.Get<IEndpointFeature>();
|
||||
var endpoint = endpointFeature?.Endpoint;
|
||||
|
||||
Log.NoComponentsRegisteredInEndpoint(_logger, endpoint.DisplayName);
|
||||
|
||||
// No components preregistered so return. This is totally normal if the components were prerendered.
|
||||
Log.NoComponentsRegisteredInEndpoint(_logger, Context.GetHttpContext().GetEndpoint()?.DisplayName);
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var circuitHost = _circuitFactory.CreateCircuitHost(
|
||||
var circuitClient = new CircuitClientProxy(Clients.Caller, Context.ConnectionId);
|
||||
circuitHost = _circuitFactory.CreateCircuitHost(
|
||||
Context.GetHttpContext(),
|
||||
circuitClient,
|
||||
baseUri,
|
||||
uri,
|
||||
Context.User);
|
||||
|
||||
circuitHost.UnhandledException += CircuitHost_UnhandledException;
|
||||
|
||||
// Fire-and-forget the initialization process, because we can't block the
|
||||
// SignalR message loop (we'd get a deadlock if any of the initialization
|
||||
// logic relied on receiving a subsequent message from SignalR), and it will
|
||||
|
|
@ -127,141 +130,136 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
// It's safe to *publish* the circuit now because nothing will be able
|
||||
// to run inside it until after InitializeAsync completes.
|
||||
_circuitRegistry.Register(circuitHost);
|
||||
CircuitHost = circuitHost;
|
||||
SetCircuit(circuitHost);
|
||||
return circuitHost.CircuitId;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// If the circuit fails to initialize synchronously we can notify the client immediately
|
||||
// and shut down the connection.
|
||||
Log.CircuitInitializationFailed(_logger, ex);
|
||||
NotifyClientError(Clients.Caller, "The circuit failed to initialize.");
|
||||
await NotifyClientError(Clients.Caller, "The circuit failed to initialize.");
|
||||
Context.Abort();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Intended for framework use only. Applications should not call this method directly.
|
||||
/// </summary>
|
||||
public async Task<bool> ConnectCircuit(string circuitId)
|
||||
public async ValueTask<bool> ConnectCircuit(string circuitId)
|
||||
{
|
||||
// ConnectionAsync will not throw.
|
||||
var circuitHost = await _circuitRegistry.ConnectAsync(circuitId, Clients.Caller, Context.ConnectionId, Context.ConnectionAborted);
|
||||
if (circuitHost != null)
|
||||
{
|
||||
CircuitHost = circuitHost;
|
||||
CircuitHost.UnhandledException += CircuitHost_UnhandledException;
|
||||
|
||||
SetCircuit(circuitHost);
|
||||
circuitHost.SetCircuitUser(Context.User);
|
||||
circuitHost.SendPendingBatches();
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we get here the circuit does not exist anymore. This is something that's valid for a client to
|
||||
// recover from, and the client is not holding any resources right now other than the connection.
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Intended for framework use only. Applications should not call this method directly.
|
||||
/// </summary>
|
||||
public void BeginInvokeDotNetFromJS(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson)
|
||||
public async ValueTask BeginInvokeDotNetFromJS(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson)
|
||||
{
|
||||
if (CircuitHost == null)
|
||||
var circuitHost = await GetActiveCircuitAsync();
|
||||
if (circuitHost == null)
|
||||
{
|
||||
Log.CircuitHostNotInitialized(_logger);
|
||||
_ = NotifyClientError(Clients.Caller, "Circuit not initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
_ = CircuitHost.BeginInvokeDotNetFromJS(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson);
|
||||
_ = circuitHost.BeginInvokeDotNetFromJS(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Intended for framework use only. Applications should not call this method directly.
|
||||
/// </summary>
|
||||
public void EndInvokeJSFromDotNet(long asyncHandle, bool succeeded, string arguments)
|
||||
public async ValueTask EndInvokeJSFromDotNet(long asyncHandle, bool succeeded, string arguments)
|
||||
{
|
||||
if (CircuitHost == null)
|
||||
var circuitHost = await GetActiveCircuitAsync();
|
||||
if (circuitHost == null)
|
||||
{
|
||||
Log.CircuitHostNotInitialized(_logger);
|
||||
_ = NotifyClientError(Clients.Caller, "Circuit not initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
_ = CircuitHost.EndInvokeJSFromDotNet(asyncHandle, succeeded, arguments);
|
||||
_ = circuitHost.EndInvokeJSFromDotNet(asyncHandle, succeeded, arguments);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Intended for framework use only. Applications should not call this method directly.
|
||||
/// </summary>
|
||||
public void DispatchBrowserEvent(string eventDescriptor, string eventArgs)
|
||||
public async ValueTask DispatchBrowserEvent(string eventDescriptor, string eventArgs)
|
||||
{
|
||||
if (CircuitHost == null)
|
||||
var circuitHost = await GetActiveCircuitAsync();
|
||||
if (circuitHost == null)
|
||||
{
|
||||
Log.CircuitHostNotInitialized(_logger);
|
||||
_ = NotifyClientError(Clients.Caller, "Circuit not initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
_ = CircuitHost.DispatchEvent(eventDescriptor, eventArgs);
|
||||
_ = circuitHost.DispatchEvent(eventDescriptor, eventArgs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Intended for framework use only. Applications should not call this method directly.
|
||||
/// </summary>
|
||||
public void OnRenderCompleted(long renderId, string errorMessageOrNull)
|
||||
public async ValueTask OnRenderCompleted(long renderId, string errorMessageOrNull)
|
||||
{
|
||||
if (CircuitHost == null)
|
||||
var circuitHost = await GetActiveCircuitAsync();
|
||||
if (circuitHost == null)
|
||||
{
|
||||
Log.CircuitHostNotInitialized(_logger);
|
||||
NotifyClientError(Clients.Caller, "Circuit not initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.ReceivedConfirmationForBatch(_logger, renderId);
|
||||
_ = CircuitHost.Renderer.OnRenderCompleted(renderId, errorMessageOrNull);
|
||||
_ = circuitHost.Renderer.OnRenderCompleted(renderId, errorMessageOrNull);
|
||||
}
|
||||
|
||||
public void OnLocationChanged(string uri, bool intercepted)
|
||||
public async ValueTask OnLocationChanged(string uri, bool intercepted)
|
||||
{
|
||||
if (CircuitHost == null)
|
||||
var circuitHost = await GetActiveCircuitAsync();
|
||||
if (circuitHost == null)
|
||||
{
|
||||
Log.CircuitHostNotInitialized(_logger);
|
||||
NotifyClientError(Clients.Caller, "Circuit not initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
_ = CircuitHost.OnLocationChangedAsync(uri, intercepted);
|
||||
_ = circuitHost.OnLocationChangedAsync(uri, intercepted);
|
||||
}
|
||||
|
||||
private async void CircuitHost_UnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||
// We store the CircuitHost through a *handle* here because Context.Items is tied to the lifetime
|
||||
// of the connection. It's possible that a misbehaving client could cause disposal of a CircuitHost
|
||||
// but keep a connection open indefinitely, preventing GC of the Circuit and related application state.
|
||||
// Using a handle allows the CircuitHost to clear this reference in the background.
|
||||
//
|
||||
// See comment on error handling on the class definition.
|
||||
private async ValueTask<CircuitHost> GetActiveCircuitAsync([CallerMemberName] string callSite = "")
|
||||
{
|
||||
var circuitHost = (CircuitHost)sender;
|
||||
var circuitId = circuitHost?.CircuitId;
|
||||
|
||||
try
|
||||
var handle = (CircuitHandle)Context.Items[CircuitKey];
|
||||
var circuitHost = handle?.CircuitHost;
|
||||
if (handle != null && circuitHost == null)
|
||||
{
|
||||
Log.UnhandledExceptionInCircuit(_logger, circuitId, (Exception)e.ExceptionObject);
|
||||
if (_options.DetailedErrors)
|
||||
{
|
||||
await NotifyClientError(circuitHost.Client, e.ExceptionObject.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
var message = $"There was an unhandled exception on the current circuit, so this circuit will be terminated. For more details turn on " +
|
||||
$"detailed exceptions in '{typeof(CircuitOptions).Name}.{nameof(CircuitOptions.DetailedErrors)}'";
|
||||
|
||||
await NotifyClientError(circuitHost.Client, message);
|
||||
}
|
||||
|
||||
// We generally can't abort the connection here since this is an async
|
||||
// callback. The Hub has already been torn down. We'll rely on the
|
||||
// client to abort the connection if we successfully transmit an error.
|
||||
// This can occur when a circuit host does not exist anymore due to an unhandled exception.
|
||||
// We can reject this and terminate the connection.
|
||||
Log.CircuitHostShutdown(_logger, callSite);
|
||||
await NotifyClientError(Clients.Caller, "Circuit has been shut down due to error.");
|
||||
Context.Abort();
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
else if (circuitHost == null)
|
||||
{
|
||||
Log.FailedToTransmitException(_logger, circuitId, ex);
|
||||
// This can occur when a circuit host does not exist anymore due to an unhandled exception.
|
||||
// We can reject this and terminate the connection.
|
||||
Log.CircuitHostNotInitialized(_logger, callSite);
|
||||
await NotifyClientError(Clients.Caller, "Circuit not initialized.");
|
||||
Context.Abort();
|
||||
return null;
|
||||
}
|
||||
|
||||
return circuitHost;
|
||||
}
|
||||
|
||||
private static Task NotifyClientError(IClientProxy client, string error) =>
|
||||
client.SendAsync("JS.Error", error);
|
||||
private CircuitHost GetCircuit()
|
||||
{
|
||||
return ((CircuitHandle)Context.Items[CircuitKey])?.CircuitHost;
|
||||
}
|
||||
|
||||
private void SetCircuit(CircuitHost circuitHost)
|
||||
{
|
||||
Context.Items[CircuitKey] = circuitHost?.Handle;
|
||||
}
|
||||
|
||||
private static Task NotifyClientError(IClientProxy client, string error) => client.SendAsync("JS.Error", error);
|
||||
|
||||
private static class Log
|
||||
{
|
||||
|
|
@ -274,14 +272,14 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
private static readonly Action<ILogger, string, Exception> _unhandledExceptionInCircuit =
|
||||
LoggerMessage.Define<string>(LogLevel.Warning, new EventId(3, "UnhandledExceptionInCircuit"), "Unhandled exception in circuit {CircuitId}");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _failedToTransmitException =
|
||||
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(4, "FailedToTransmitException"), "Failed to transmit exception to client in circuit {CircuitId}");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _circuitAlreadyInitialized =
|
||||
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(5, "CircuitAlreadyInitialized"), "The circuit host '{CircuitId}' has already been initialized");
|
||||
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(4, "CircuitAlreadyInitialized"), "The circuit host '{CircuitId}' has already been initialized");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _circuitHostNotInitialized =
|
||||
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(6, "CircuitHostNotInitialized"), "Call to '{CallSite}' received before the circuit host initialization");
|
||||
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(5, "CircuitHostNotInitialized"), "Call to '{CallSite}' received before the circuit host initialization");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _circuitHostShutdown =
|
||||
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(6, "CircuitHostShutdown"), "Call to '{CallSite}' received after the circuit was shut down");
|
||||
|
||||
private static readonly Action<ILogger, string, Exception> _circuitTerminatedGracefully =
|
||||
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(7, "CircuitTerminatedGracefully"), "Circuit '{CircuitId}' terminated gracefully");
|
||||
|
|
@ -307,15 +305,12 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
_unhandledExceptionInCircuit(logger, circuitId, exception);
|
||||
}
|
||||
|
||||
public static void FailedToTransmitException(ILogger logger, string circuitId, Exception transmissionException)
|
||||
{
|
||||
_failedToTransmitException(logger, circuitId, transmissionException);
|
||||
}
|
||||
|
||||
public static void CircuitAlreadyInitialized(ILogger logger, string circuitId) => _circuitAlreadyInitialized(logger, circuitId, null);
|
||||
|
||||
public static void CircuitHostNotInitialized(ILogger logger, [CallerMemberName] string callSite = "") => _circuitHostNotInitialized(logger, callSite, null);
|
||||
|
||||
public static void CircuitHostShutdown(ILogger logger, [CallerMemberName] string callSite = "") => _circuitHostShutdown(logger, callSite, null);
|
||||
|
||||
public static void CircuitTerminatedGracefully(ILogger logger, string circuitId) => _circuitTerminatedGracefully(logger, circuitId, null);
|
||||
|
||||
public static void InvalidInputData(ILogger logger, [CallerMemberName] string callSite = "") => _invalidInputData(logger, callSite, null);
|
||||
|
|
|
|||
|
|
@ -38,10 +38,11 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
// Assert
|
||||
serviceScope.Verify(s => s.Dispose(), Times.Once());
|
||||
Assert.True(remoteRenderer.Disposed);
|
||||
Assert.Null(circuitHost.Handle.CircuitHost);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisposeAsync_DisposesResourcesEvenIfCircuitHandlerOrComponentThrows()
|
||||
public async Task DisposeAsync_DisposesResourcesAndSilencesException()
|
||||
{
|
||||
// Arrange
|
||||
var serviceScope = new Mock<IServiceScope>();
|
||||
|
|
@ -60,10 +61,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
circuitHost.Renderer.AssignRootComponentId(throwOnDisposeComponent);
|
||||
|
||||
// Act
|
||||
await Assert.ThrowsAsync<InvalidTimeZoneException>(async () =>
|
||||
{
|
||||
await circuitHost.DisposeAsync();
|
||||
});
|
||||
await circuitHost.DisposeAsync(); // Does not throw
|
||||
|
||||
// Assert
|
||||
Assert.True(throwOnDisposeComponent.DidCallDispose);
|
||||
|
|
@ -181,7 +179,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
await initializeAsyncTask;
|
||||
|
||||
// Assert: The async exception was reported via the side-channel
|
||||
Assert.Same(ex, reportedErrors.Single().ExceptionObject);
|
||||
var aex = Assert.IsType<AggregateException>(reportedErrors.Single().ExceptionObject);
|
||||
Assert.Same(ex, aex.InnerExceptions.Single());
|
||||
Assert.False(reportedErrors.Single().IsTerminating);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -133,6 +133,30 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
handler.Verify(v => v.OnCircuitClosedAsync(It.IsAny<Circuit>(), It.IsAny<CancellationToken>()), Times.Never());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectAsync_InvokesCircuitHandlers_DisposesCircuitOnFailure()
|
||||
{
|
||||
// Arrange
|
||||
var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory();
|
||||
var registry = CreateRegistry(circuitIdFactory);
|
||||
var handler = new Mock<CircuitHandler> { CallBase = true };
|
||||
handler.Setup(h => h.OnConnectionUpAsync(It.IsAny<Circuit>(), It.IsAny<CancellationToken>())).Throws(new InvalidTimeZoneException());
|
||||
var circuitHost = TestCircuitHost.Create(circuitIdFactory.CreateCircuitId(), handlers: new[] { handler.Object });
|
||||
registry.Register(circuitHost);
|
||||
|
||||
var newClient = Mock.Of<IClientProxy>();
|
||||
var newConnectionId = "new-id";
|
||||
|
||||
// Act
|
||||
var result = await registry.ConnectAsync(circuitHost.CircuitId, newClient, newConnectionId, default);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
Assert.Null(circuitHost.Handle.CircuitHost); // Will be null if disposed.
|
||||
Assert.Empty(registry.ConnectedCircuits);
|
||||
Assert.Equal(0, registry.DisconnectedCircuits.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisconnectAsync_DoesNothing_IfCircuitIsInactive()
|
||||
{
|
||||
|
|
@ -409,7 +433,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
protected override void OnEntryEvicted(object key, object value, EvictionReason reason, object state)
|
||||
{
|
||||
base.OnEntryEvicted(key, value, reason, state);
|
||||
OnAfterEntryEvicted();
|
||||
OnAfterEntryEvicted?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.Web.Rendering;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
|
@ -18,16 +16,11 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
{
|
||||
internal class TestCircuitHost : CircuitHost
|
||||
{
|
||||
private TestCircuitHost(string circuitId, IServiceScope scope, CircuitClientProxy client, RemoteRenderer renderer, IReadOnlyList<ComponentDescriptor> descriptors, RemoteJSRuntime jsRuntime, CircuitHandler[] circuitHandlers, ILogger logger)
|
||||
: base(circuitId, scope, client, renderer, descriptors, jsRuntime, circuitHandlers, logger)
|
||||
private TestCircuitHost(string circuitId, IServiceScope scope, CircuitOptions options, CircuitClientProxy client, RemoteRenderer renderer, IReadOnlyList<ComponentDescriptor> descriptors, RemoteJSRuntime jsRuntime, CircuitHandler[] circuitHandlers, ILogger logger)
|
||||
: base(circuitId, scope, options, client, renderer, descriptors, jsRuntime, circuitHandlers, logger)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void OnHandlerError(CircuitHandler circuitHandler, string handlerMethod, Exception ex)
|
||||
{
|
||||
ExceptionDispatchInfo.Capture(ex).Throw();
|
||||
}
|
||||
|
||||
public static CircuitHost Create(
|
||||
string circuitId = null,
|
||||
IServiceScope serviceScope = null,
|
||||
|
|
@ -55,6 +48,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
return new TestCircuitHost(
|
||||
circuitId ?? Guid.NewGuid().ToString(),
|
||||
serviceScope,
|
||||
new CircuitOptions(),
|
||||
clientProxy,
|
||||
remoteRenderer,
|
||||
new List<ComponentDescriptor>(),
|
||||
|
|
|
|||
|
|
@ -16,9 +16,10 @@
|
|||
<!-- Tests do not work on Helix or when bin/ directory is not in project directory due to undeclared dependency on test content. -->
|
||||
<BaseOutputPath />
|
||||
|
||||
|
||||
<OutputPath />
|
||||
|
||||
<GenerateLoggingTestingAssemblyAttributes>false</GenerateLoggingTestingAssemblyAttributes>
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
@ -29,6 +30,7 @@
|
|||
<Reference Include="Microsoft.AspNetCore.Hosting" />
|
||||
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
|
||||
<Reference Include="Microsoft.AspNetCore.StaticFiles" />
|
||||
<Reference Include="Microsoft.Extensions.Logging.Testing" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Ignitor;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||
|
|
@ -11,26 +14,29 @@ using Microsoft.Extensions.DependencyInjection;
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
||||
{
|
||||
public class ComponentHubReliabilityTest : IClassFixture<AspNetSiteServerFixture>, IDisposable
|
||||
{
|
||||
private static readonly TimeSpan DefaultLatencyTimeout = TimeSpan.FromMilliseconds(500);
|
||||
private static readonly TimeSpan DefaultLatencyTimeout = TimeSpan.FromSeconds(10);
|
||||
private readonly AspNetSiteServerFixture _serverFixture;
|
||||
|
||||
public ComponentHubReliabilityTest(AspNetSiteServerFixture serverFixture)
|
||||
public ComponentHubReliabilityTest(AspNetSiteServerFixture serverFixture, ITestOutputHelper output)
|
||||
{
|
||||
serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost;
|
||||
_serverFixture = serverFixture;
|
||||
Output = output;
|
||||
|
||||
serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost;
|
||||
CreateDefaultConfiguration();
|
||||
}
|
||||
|
||||
public BlazorClient Client { get; set; }
|
||||
|
||||
public ITestOutputHelper Output { get; set; }
|
||||
private IList<Batch> Batches { get; set; } = new List<Batch>();
|
||||
private IList<string> Errors { get; set; } = new List<string>();
|
||||
private IList<LogMessage> Logs { get; set; } = new List<LogMessage>();
|
||||
private ConcurrentQueue<LogMessage> Logs { get; set; } = new ConcurrentQueue<LogMessage>();
|
||||
|
||||
public TestSink TestSink { get; set; }
|
||||
|
||||
|
|
@ -39,13 +45,24 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
Client = new BlazorClient() { DefaultLatencyTimeout = DefaultLatencyTimeout };
|
||||
Client.RenderBatchReceived += (id, data) => Batches.Add(new Batch(id, data));
|
||||
Client.OnCircuitError += (error) => Errors.Add(error);
|
||||
Client.LoggerProvider = new XunitLoggerProvider(Output);
|
||||
Client.FormatError = (error) =>
|
||||
{
|
||||
var logs = string.Join(Environment.NewLine, Logs);
|
||||
return new Exception(error + Environment.NewLine + logs);
|
||||
};
|
||||
|
||||
_ = _serverFixture.RootUri; // this is needed for the side-effects of getting the URI.
|
||||
TestSink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
|
||||
TestSink.MessageLogged += LogMessages;
|
||||
}
|
||||
|
||||
private void LogMessages(WriteContext context) => Logs.Add(new LogMessage(context.LogLevel, context.Message, context.Exception));
|
||||
private void LogMessages(WriteContext context)
|
||||
{
|
||||
var log = new LogMessage(context.LogLevel, context.Message, context.Exception);
|
||||
Logs.Enqueue(log);
|
||||
Output.WriteLine(log.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CannotStartMultipleCircuits()
|
||||
|
|
@ -58,7 +75,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
Assert.Single(Batches);
|
||||
|
||||
// Act
|
||||
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
|
||||
await Client.ExpectCircuitErrorAndDisconnect(() => Client.HubConnection.SendAsync(
|
||||
"StartCircuit",
|
||||
baseUri,
|
||||
baseUri + "/home"));
|
||||
|
|
@ -79,25 +96,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
Assert.True(await Client.ConnectAsync(uri, prerendered: false, connectAutomatically: false), "Couldn't connect to the app");
|
||||
|
||||
// Act
|
||||
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync("StartCircuit", null, null));
|
||||
|
||||
// Assert
|
||||
var actualError = Assert.Single(Errors);
|
||||
Assert.Matches(expectedError, actualError);
|
||||
Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CannotStartCircuitWithInvalidUris()
|
||||
{
|
||||
// Arrange
|
||||
var expectedError = "The uris provided are invalid.";
|
||||
var rootUri = _serverFixture.RootUri;
|
||||
var uri = new Uri(rootUri, "/subdir");
|
||||
Assert.True(await Client.ConnectAsync(uri, prerendered: false, connectAutomatically: false), "Couldn't connect to the app");
|
||||
|
||||
// Act
|
||||
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync("StartCircuit", uri.AbsoluteUri, "/foo"));
|
||||
await Client.ExpectCircuitErrorAndDisconnect(() => Client.HubConnection.SendAsync("StartCircuit", null, null));
|
||||
|
||||
// Assert
|
||||
var actualError = Assert.Single(Errors);
|
||||
|
|
@ -119,7 +118,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
// Act
|
||||
//
|
||||
// These are valid URIs by the BaseUri doesn't contain the Uri - so it fails to initialize.
|
||||
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync("StartCircuit", uri, "http://example.com"), TimeSpan.FromHours(1));
|
||||
await Client.ExpectCircuitErrorAndDisconnect(() => Client.HubConnection.SendAsync("StartCircuit", uri, "http://example.com"));
|
||||
|
||||
// Assert
|
||||
var actualError = Assert.Single(Errors);
|
||||
|
|
@ -138,7 +137,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
Assert.Empty(Batches);
|
||||
|
||||
// Act
|
||||
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
|
||||
await Client.ExpectCircuitErrorAndDisconnect(() => Client.HubConnection.SendAsync(
|
||||
"BeginInvokeDotNetFromJS",
|
||||
"",
|
||||
"",
|
||||
|
|
@ -164,7 +163,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
Assert.Empty(Batches);
|
||||
|
||||
// Act
|
||||
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
|
||||
await Client.ExpectCircuitErrorAndDisconnect(() => Client.HubConnection.SendAsync(
|
||||
"EndInvokeJSFromDotNet",
|
||||
3,
|
||||
true,
|
||||
|
|
@ -188,7 +187,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
Assert.Empty(Batches);
|
||||
|
||||
// Act
|
||||
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
|
||||
await Client.ExpectCircuitErrorAndDisconnect(() => Client.HubConnection.SendAsync(
|
||||
"DispatchBrowserEvent",
|
||||
"",
|
||||
""));
|
||||
|
|
@ -201,7 +200,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CannotInvokeOnRenderCompletedInitialization()
|
||||
public async Task CannotInvokeOnRenderCompletedBeforeInitialization()
|
||||
{
|
||||
// Arrange
|
||||
var expectedError = "Circuit not initialized.";
|
||||
|
|
@ -211,7 +210,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
Assert.Empty(Batches);
|
||||
|
||||
// Act
|
||||
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
|
||||
await Client.ExpectCircuitErrorAndDisconnect(() => Client.HubConnection.SendAsync(
|
||||
"OnRenderCompleted",
|
||||
5,
|
||||
null));
|
||||
|
|
@ -234,7 +233,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
Assert.Empty(Batches);
|
||||
|
||||
// Act
|
||||
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
|
||||
await Client.ExpectCircuitErrorAndDisconnect(() => Client.HubConnection.SendAsync(
|
||||
"OnLocationChanged",
|
||||
baseUri.AbsoluteUri,
|
||||
false));
|
||||
|
|
@ -246,6 +245,129 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
Assert.Contains(Logs, l => (l.LogLevel, l.Message) == (LogLevel.Debug, "Call to 'OnLocationChanged' received before the circuit host initialization"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnLocationChanged_ReportsDebugForExceptionInValidation()
|
||||
{
|
||||
// Arrange
|
||||
var expectedError = "Location change to http://example.com failed.";
|
||||
var rootUri = _serverFixture.RootUri;
|
||||
var baseUri = new Uri(rootUri, "/subdir");
|
||||
Assert.True(await Client.ConnectAsync(baseUri, prerendered: false), "Couldn't connect to the app");
|
||||
Assert.Single(Batches);
|
||||
|
||||
// Act
|
||||
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
|
||||
"OnLocationChanged",
|
||||
"http://example.com",
|
||||
false));
|
||||
|
||||
// Assert
|
||||
var actualError = Assert.Single(Errors);
|
||||
Assert.Equal(expectedError, actualError);
|
||||
Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information);
|
||||
Assert.Contains(Logs, l =>
|
||||
{
|
||||
return l.LogLevel == LogLevel.Debug && Regex.IsMatch(l.Message, "Location change to http://example.com in circuit .* failed.");
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnLocationChanged_ReportsErrorForExceptionInUserCode()
|
||||
{
|
||||
// Arrange
|
||||
var expectedError = "There was an unhandled exception .?";
|
||||
var rootUri = _serverFixture.RootUri;
|
||||
var baseUri = new Uri(rootUri, "/subdir");
|
||||
Assert.True(await Client.ConnectAsync(baseUri, prerendered: false), "Couldn't connect to the app");
|
||||
Assert.Single(Batches);
|
||||
|
||||
await Client.SelectAsync("test-selector-select", "BasicTestApp.NavigationFailureComponent");
|
||||
|
||||
// Act
|
||||
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
|
||||
"OnLocationChanged",
|
||||
new Uri(baseUri, "/test").AbsoluteUri,
|
||||
false));
|
||||
|
||||
// Assert
|
||||
var actualError = Assert.Single(Errors);
|
||||
Assert.Matches(expectedError, actualError);
|
||||
Assert.Contains(Logs, l =>
|
||||
{
|
||||
return l.LogLevel == LogLevel.Error && Regex.IsMatch(l.Message, "Unhandled exception in circuit .*");
|
||||
});
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("constructor-throw")]
|
||||
[InlineData("attach-throw")]
|
||||
[InlineData("setparameters-sync-throw")]
|
||||
[InlineData("setparameters-async-throw")]
|
||||
[InlineData("render-throw")]
|
||||
[InlineData("afterrender-sync-throw")]
|
||||
[InlineData("afterrender-async-throw")]
|
||||
public async Task ComponentLifecycleMethodThrowsExceptionTerminatesTheCircuit(string id)
|
||||
{
|
||||
// Arrange
|
||||
var expectedError = "Unhandled exception in circuit .*";
|
||||
var rootUri = _serverFixture.RootUri;
|
||||
var baseUri = new Uri(rootUri, "/subdir");
|
||||
Assert.True(await Client.ConnectAsync(baseUri, prerendered: false), "Couldn't connect to the app");
|
||||
Assert.Single(Batches);
|
||||
|
||||
await Client.SelectAsync("test-selector-select", "BasicTestApp.ReliabilityComponent");
|
||||
|
||||
// Act
|
||||
await Client.ExpectCircuitError(async () =>
|
||||
{
|
||||
await Client.ClickAsync(id, expectRenderBatch: false);
|
||||
});
|
||||
|
||||
// Now if you try to click again, you will get *forcibly* disconnected for trying to talk to
|
||||
// a circuit that's gone.
|
||||
await Client.ExpectCircuitErrorAndDisconnect(async () =>
|
||||
{
|
||||
await Assert.ThrowsAsync<TaskCanceledException>(async () => await Client.ClickAsync(id, expectRenderBatch: false));
|
||||
});
|
||||
|
||||
// Checking logs at the end to avoid race condition.
|
||||
Assert.Contains(
|
||||
Logs,
|
||||
e => LogLevel.Error == e.LogLevel && Regex.IsMatch(e.Message, expectedError));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ComponentDisposeMethodThrowsExceptionTerminatesTheCircuit()
|
||||
{
|
||||
// Arrange
|
||||
var expectedError = "Unhandled exception in circuit .*";
|
||||
var rootUri = _serverFixture.RootUri;
|
||||
var baseUri = new Uri(rootUri, "/subdir");
|
||||
Assert.True(await Client.ConnectAsync(baseUri, prerendered: false), "Couldn't connect to the app");
|
||||
Assert.Single(Batches);
|
||||
|
||||
await Client.SelectAsync("test-selector-select", "BasicTestApp.ReliabilityComponent");
|
||||
|
||||
// Act - show then hide
|
||||
await Client.ClickAsync("dispose-throw");
|
||||
await Client.ExpectCircuitError(async () =>
|
||||
{
|
||||
await Client.ClickAsync("dispose-throw", expectRenderBatch: false);
|
||||
});
|
||||
|
||||
// Now if you try to click again, you will get *forcibly* disconnected for trying to talk to
|
||||
// a circuit that's gone.
|
||||
await Client.ExpectCircuitErrorAndDisconnect(async () =>
|
||||
{
|
||||
await Assert.ThrowsAsync<TaskCanceledException>(async () => await Client.ClickAsync("dispose-throw", expectRenderBatch: false));
|
||||
});
|
||||
|
||||
// Checking logs at the end to avoid race condition.
|
||||
Assert.Contains(
|
||||
Logs,
|
||||
e => LogLevel.Error == e.LogLevel && Regex.IsMatch(e.Message, expectedError));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
TestSink.MessageLogged -= LogMessages;
|
||||
|
|
@ -263,6 +385,11 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
public LogLevel LogLevel { get; set; }
|
||||
public string Message { get; set; }
|
||||
public Exception Exception { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{LogLevel}: {Message}{(Exception != null ? Environment.NewLine : "")}{Exception}";
|
||||
}
|
||||
}
|
||||
|
||||
private class Batch
|
||||
|
|
|
|||
|
|
@ -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.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
|
|
@ -10,28 +11,63 @@ using Ignitor;
|
|||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Microsoft.AspNetCore.Testing.xunit;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
||||
{
|
||||
[Flaky("https://github.com/aspnet/AspNetCore/issues/12940", FlakyOn.All)]
|
||||
public class InteropReliabilityTests : IClassFixture<AspNetSiteServerFixture>
|
||||
public class InteropReliabilityTests : IClassFixture<AspNetSiteServerFixture>, IDisposable
|
||||
{
|
||||
private static readonly TimeSpan DefaultLatencyTimeout = TimeSpan.FromSeconds(5);
|
||||
private static readonly TimeSpan DefaultLatencyTimeout = TimeSpan.FromSeconds(30);
|
||||
private readonly AspNetSiteServerFixture _serverFixture;
|
||||
|
||||
public InteropReliabilityTests(AspNetSiteServerFixture serverFixture)
|
||||
public InteropReliabilityTests(AspNetSiteServerFixture serverFixture, ITestOutputHelper output)
|
||||
{
|
||||
serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost;
|
||||
_serverFixture = serverFixture;
|
||||
Output = output;
|
||||
|
||||
serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost;
|
||||
CreateDefaultConfiguration();
|
||||
}
|
||||
|
||||
public BlazorClient Client { get; set; } = new BlazorClient() { DefaultLatencyTimeout = DefaultLatencyTimeout };
|
||||
public BlazorClient Client { get; set; }
|
||||
public ITestOutputHelper Output { get; set; }
|
||||
private IList<Batch> Batches { get; set; } = new List<Batch>();
|
||||
private List<DotNetCompletion> DotNetCompletions = new List<DotNetCompletion>();
|
||||
private List<JSInteropCall> JSInteropCalls = new List<JSInteropCall>();
|
||||
private IList<string> Errors { get; set; } = new List<string>();
|
||||
private ConcurrentQueue<LogMessage> Logs { get; set; } = new ConcurrentQueue<LogMessage>();
|
||||
|
||||
public TestSink TestSink { get; set; }
|
||||
|
||||
private void CreateDefaultConfiguration()
|
||||
{
|
||||
Client = new BlazorClient() { DefaultLatencyTimeout = DefaultLatencyTimeout };
|
||||
Client.RenderBatchReceived += (id, data) => Batches.Add(new Batch(id, data));
|
||||
Client.DotNetInteropCompletion += (method) => DotNetCompletions.Add(new DotNetCompletion(method));
|
||||
Client.JSInterop += (asyncHandle, identifier, argsJson) => JSInteropCalls.Add(new JSInteropCall(asyncHandle, identifier, argsJson));
|
||||
Client.OnCircuitError += (error) => Errors.Add(error);
|
||||
Client.LoggerProvider = new XunitLoggerProvider(Output);
|
||||
Client.FormatError = (error) =>
|
||||
{
|
||||
var logs = string.Join(Environment.NewLine, Logs);
|
||||
return new Exception(error + Environment.NewLine + logs);
|
||||
};
|
||||
|
||||
_ = _serverFixture.RootUri; // this is needed for the side-effects of getting the URI.
|
||||
TestSink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
|
||||
TestSink.MessageLogged += LogMessages;
|
||||
}
|
||||
|
||||
private void LogMessages(WriteContext context)
|
||||
{
|
||||
var log = new LogMessage(context.LogLevel, context.Message, context.Exception);
|
||||
Logs.Enqueue(log);
|
||||
Output.WriteLine(log.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CannotInvokeNonJSInvokableMethods()
|
||||
|
|
@ -40,8 +76,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
var expectedError = "[\"1\"," +
|
||||
"false," +
|
||||
"\"There was an exception invoking \\u0027WriteAllText\\u0027 on assembly \\u0027System.IO.FileSystem\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]";
|
||||
var (_, dotNetCompletions, batches) = ConfigureClient();
|
||||
await GoToTestComponent(batches);
|
||||
await GoToTestComponent(Batches);
|
||||
|
||||
// Act
|
||||
await Client.InvokeDotNetMethod(
|
||||
|
|
@ -52,9 +87,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
JsonSerializer.Serialize(new[] { ".\\log.txt", "log" }));
|
||||
|
||||
// Assert
|
||||
Assert.Single(dotNetCompletions, expectedError);
|
||||
|
||||
await ValidateClientKeepsWorking(Client, batches);
|
||||
Assert.Single(DotNetCompletions, c => c.Message == expectedError);
|
||||
await ValidateClientKeepsWorking(Client, Batches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -64,8 +98,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
var expectedError = "[\"1\"," +
|
||||
"false," +
|
||||
"\"There was an exception invoking \\u0027MadeUpMethod\\u0027 on assembly \\u0027BasicTestApp\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]";
|
||||
var (_, dotNetCompletions, batches) = ConfigureClient();
|
||||
await GoToTestComponent(batches);
|
||||
|
||||
await GoToTestComponent(Batches);
|
||||
|
||||
// Act
|
||||
await Client.InvokeDotNetMethod(
|
||||
|
|
@ -76,19 +110,19 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
JsonSerializer.Serialize(new[] { ".\\log.txt", "log" }));
|
||||
|
||||
// Assert
|
||||
Assert.Single(dotNetCompletions, expectedError);
|
||||
await ValidateClientKeepsWorking(Client, batches);
|
||||
Assert.Single(DotNetCompletions, c => c.Message == expectedError);
|
||||
await ValidateClientKeepsWorking(Client, Batches);
|
||||
}
|
||||
|
||||
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/12940")]
|
||||
[Fact]
|
||||
public async Task CannotInvokeJSInvokableMethodsWithWrongNumberOfArguments()
|
||||
{
|
||||
// Arrange
|
||||
var expectedError = "[\"1\"," +
|
||||
"false," +
|
||||
"\"There was an exception invoking \\u0027NotifyLocationChanged\\u0027 on assembly \\u0027Microsoft.AspNetCore.Components.Server\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]";
|
||||
var (_, dotNetCompletions, batches) = ConfigureClient();
|
||||
await GoToTestComponent(batches);
|
||||
|
||||
await GoToTestComponent(Batches);
|
||||
|
||||
// Act
|
||||
await Client.InvokeDotNetMethod(
|
||||
|
|
@ -99,9 +133,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
JsonSerializer.Serialize(new[] { _serverFixture.RootUri }));
|
||||
|
||||
// Assert
|
||||
Assert.Single(dotNetCompletions, expectedError);
|
||||
|
||||
await ValidateClientKeepsWorking(Client, batches);
|
||||
Assert.Single(DotNetCompletions, c => c.Message == expectedError);
|
||||
await ValidateClientKeepsWorking(Client, Batches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -111,8 +144,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
var expectedError = "[\"1\"," +
|
||||
"false," +
|
||||
"\"There was an exception invoking \\u0027NotifyLocationChanged\\u0027 on assembly \\u0027\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]";
|
||||
var (_, dotNetCompletions, batches) = ConfigureClient();
|
||||
await GoToTestComponent(batches);
|
||||
|
||||
await GoToTestComponent(Batches);
|
||||
|
||||
// Act
|
||||
await Client.InvokeDotNetMethod(
|
||||
|
|
@ -123,9 +156,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
JsonSerializer.Serialize(new object[] { _serverFixture.RootUri + "counter", false }));
|
||||
|
||||
// Assert
|
||||
Assert.Single(dotNetCompletions, expectedError);
|
||||
|
||||
await ValidateClientKeepsWorking(Client, batches);
|
||||
Assert.Single(DotNetCompletions, c => c.Message == expectedError);
|
||||
await ValidateClientKeepsWorking(Client, Batches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -135,8 +167,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
var expectedError = "[\"1\"," +
|
||||
"false," +
|
||||
"\"There was an exception invoking \\u0027\\u0027 on assembly \\u0027Microsoft.AspNetCore.Components.Server\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]";
|
||||
var (_, dotNetCompletions, batches) = ConfigureClient();
|
||||
await GoToTestComponent(batches);
|
||||
|
||||
await GoToTestComponent(Batches);
|
||||
|
||||
// Act
|
||||
await Client.InvokeDotNetMethod(
|
||||
|
|
@ -147,9 +179,9 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
JsonSerializer.Serialize(new object[] { _serverFixture.RootUri + "counter", false }));
|
||||
|
||||
// Assert
|
||||
Assert.Single(dotNetCompletions, expectedError);
|
||||
Assert.Single(DotNetCompletions, c => c.Message == expectedError);
|
||||
|
||||
await ValidateClientKeepsWorking(Client, batches);
|
||||
await ValidateClientKeepsWorking(Client, Batches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -160,8 +192,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
var expectedError = "[\"1\"," +
|
||||
"false," +
|
||||
"\"There was an exception invoking \\u0027Reverse\\u0027 on assembly \\u0027\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]";
|
||||
var (_, dotNetCompletions, batches) = ConfigureClient();
|
||||
await GoToTestComponent(batches);
|
||||
|
||||
await GoToTestComponent(Batches);
|
||||
|
||||
// Act
|
||||
await Client.InvokeDotNetMethod(
|
||||
|
|
@ -171,7 +203,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
null,
|
||||
JsonSerializer.Serialize(Array.Empty<object>()));
|
||||
|
||||
Assert.Single(dotNetCompletions, expectedDotNetObjectRef);
|
||||
Assert.Single(DotNetCompletions, c => c.Message == expectedDotNetObjectRef);
|
||||
|
||||
await Client.InvokeDotNetMethod(
|
||||
"1",
|
||||
|
|
@ -181,7 +213,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
JsonSerializer.Serialize(Array.Empty<object>()));
|
||||
|
||||
// Assert
|
||||
Assert.Single(dotNetCompletions, "[\"1\",true,\"tnatropmI\"]");
|
||||
Assert.Single(DotNetCompletions, c => c.Message == "[\"1\",true,\"tnatropmI\"]");
|
||||
|
||||
await Client.InvokeDotNetMethod(
|
||||
"1",
|
||||
|
|
@ -190,9 +222,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
3, // non existing ref
|
||||
JsonSerializer.Serialize(Array.Empty<object>()));
|
||||
|
||||
Assert.Single(dotNetCompletions, expectedError);
|
||||
|
||||
await ValidateClientKeepsWorking(Client, batches);
|
||||
Assert.Single(DotNetCompletions, c => c.Message == expectedError);
|
||||
await ValidateClientKeepsWorking(Client, Batches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -204,8 +235,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
"false," +
|
||||
"\"There was an exception invoking \\u0027ReceiveTrivial\\u0027 on assembly \\u0027BasicTestApp\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]";
|
||||
|
||||
var (interopCalls, dotNetCompletions, batches) = ConfigureClient();
|
||||
await GoToTestComponent(batches);
|
||||
await GoToTestComponent(Batches);
|
||||
|
||||
await Client.InvokeDotNetMethod(
|
||||
"1",
|
||||
|
|
@ -214,7 +244,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
null,
|
||||
JsonSerializer.Serialize(Array.Empty<object>()));
|
||||
|
||||
Assert.Single(dotNetCompletions, expectedImportantDotNetObjectRef);
|
||||
Assert.Single(DotNetCompletions, c => c.Message == expectedImportantDotNetObjectRef);
|
||||
|
||||
// Act
|
||||
await Client.InvokeDotNetMethod(
|
||||
|
|
@ -225,9 +255,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
JsonSerializer.Serialize(new object[] { new { __dotNetObject = 1 } }));
|
||||
|
||||
// Assert
|
||||
Assert.Single(dotNetCompletions, expectedError);
|
||||
|
||||
await ValidateClientKeepsWorking(Client, batches);
|
||||
Assert.Single(DotNetCompletions, c => c.Message == expectedError);
|
||||
await ValidateClientKeepsWorking(Client, Batches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -236,16 +265,15 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
// Arrange
|
||||
var expectedError = "An exception occurred executing JS interop: The JSON value could not be converted to System.Int32. Path: $ | LineNumber: 0 | BytePositionInLine: 3.. See InnerException for more details.";
|
||||
|
||||
var (interopCalls, dotNetCompletions, batches) = ConfigureClient();
|
||||
await GoToTestComponent(batches);
|
||||
await GoToTestComponent(Batches);
|
||||
|
||||
// Act
|
||||
await Client.ClickAsync("triggerjsinterop-malformed");
|
||||
|
||||
var call = interopCalls.FirstOrDefault(call => call.identifier == "sendMalformedCallbackReturn");
|
||||
var call = JSInteropCalls.FirstOrDefault(call => call.Identifier == "sendMalformedCallbackReturn");
|
||||
Assert.NotEqual(default, call);
|
||||
|
||||
var id = call.id;
|
||||
var id = call.AsyncHandle;
|
||||
await Client.HubConnection.InvokeAsync(
|
||||
"EndInvokeJSFromDotNet",
|
||||
id,
|
||||
|
|
@ -256,41 +284,25 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
Client.FindElementById("errormessage-malformed").Children.OfType<TextNode>(),
|
||||
e => expectedError == e.TextContent);
|
||||
|
||||
await ValidateClientKeepsWorking(Client, batches);
|
||||
await ValidateClientKeepsWorking(Client, Batches);
|
||||
}
|
||||
|
||||
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/12940")]
|
||||
public async Task LogsJSInteropCompletionsCallbacksAndContinuesWorkingInAllSituations()
|
||||
[Fact]
|
||||
public async Task JSInteropCompletionSuccess()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
var (interopCalls, dotNetCompletions, batches) = ConfigureClient();
|
||||
await GoToTestComponent(batches);
|
||||
await GoToTestComponent(Batches);
|
||||
var sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
|
||||
var logEvents = new List<(LogLevel logLevel, string)>();
|
||||
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name));
|
||||
|
||||
// Act
|
||||
await Client.ClickAsync("triggerjsinterop-malformed");
|
||||
await Client.ClickAsync("triggerjsinterop-success");
|
||||
|
||||
var call = interopCalls.FirstOrDefault(call => call.identifier == "sendMalformedCallbackReturn");
|
||||
var call = JSInteropCalls.FirstOrDefault(call => call.Identifier == "sendSuccessCallbackReturn");
|
||||
Assert.NotEqual(default, call);
|
||||
|
||||
var id = call.id;
|
||||
await Client.HubConnection.InvokeAsync(
|
||||
"EndInvokeJSFromDotNet",
|
||||
id,
|
||||
true,
|
||||
$"[{id}, true, }}");
|
||||
|
||||
// A completely malformed payload like the one above never gets to the application.
|
||||
Assert.Single(
|
||||
Client.FindElementById("errormessage-malformed").Children.OfType<TextNode>(),
|
||||
e => "" == e.TextContent);
|
||||
|
||||
Assert.Contains((LogLevel.Debug, "EndInvokeDispatchException"), logEvents);
|
||||
|
||||
await Client.ClickAsync("triggerjsinterop-success");
|
||||
var id = call.AsyncHandle;
|
||||
await Client.HubConnection.InvokeAsync(
|
||||
"EndInvokeJSFromDotNet",
|
||||
id++,
|
||||
|
|
@ -302,13 +314,32 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
e => "" == e.TextContent);
|
||||
|
||||
Assert.Contains((LogLevel.Debug, "EndInvokeJSSucceeded"), logEvents);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task JSInteropThrowsInUserCode()
|
||||
{
|
||||
// Arrange
|
||||
await GoToTestComponent(Batches);
|
||||
var sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
|
||||
var logEvents = new List<(LogLevel logLevel, string)>();
|
||||
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name));
|
||||
|
||||
// Act
|
||||
await Client.ClickAsync("triggerjsinterop-failure");
|
||||
await Client.HubConnection.InvokeAsync(
|
||||
"EndInvokeJSFromDotNet",
|
||||
id++,
|
||||
false,
|
||||
$"[{id}, false, \"There was an error invoking sendFailureCallbackReturn\"]");
|
||||
|
||||
var call = JSInteropCalls.FirstOrDefault(call => call.Identifier == "sendFailureCallbackReturn");
|
||||
Assert.NotEqual(default, call);
|
||||
|
||||
var id = call.AsyncHandle;
|
||||
await Client.ExpectRenderBatch(async () =>
|
||||
{
|
||||
await Client.HubConnection.InvokeAsync(
|
||||
"EndInvokeJSFromDotNet",
|
||||
id,
|
||||
false,
|
||||
$"[{id}, false, \"There was an error invoking sendFailureCallbackReturn\"]");
|
||||
});
|
||||
|
||||
Assert.Single(
|
||||
Client.FindElementById("errormessage-failure").Children.OfType<TextNode>(),
|
||||
|
|
@ -318,7 +349,45 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
|
||||
Assert.DoesNotContain(logEvents, m => m.logLevel > LogLevel.Information);
|
||||
|
||||
await ValidateClientKeepsWorking(Client, batches);
|
||||
await ValidateClientKeepsWorking(Client, Batches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MalformedJSInteropCallbackDisposesCircuit()
|
||||
{
|
||||
// Arrange
|
||||
await GoToTestComponent(Batches);
|
||||
var sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
|
||||
var logEvents = new List<(LogLevel logLevel, string)>();
|
||||
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name));
|
||||
|
||||
// Act
|
||||
await Client.ClickAsync("triggerjsinterop-malformed");
|
||||
|
||||
var call = JSInteropCalls.FirstOrDefault(call => call.Identifier == "sendMalformedCallbackReturn");
|
||||
Assert.NotEqual(default, call);
|
||||
|
||||
var id = call.AsyncHandle;
|
||||
await Client.ExpectCircuitError(async () =>
|
||||
{
|
||||
await Client.HubConnection.InvokeAsync(
|
||||
"EndInvokeJSFromDotNet",
|
||||
id,
|
||||
true,
|
||||
$"[{id}, true, }}");
|
||||
});
|
||||
|
||||
// A completely malformed payload like the one above never gets to the application.
|
||||
Assert.Single(
|
||||
Client.FindElementById("errormessage-malformed").Children.OfType<TextNode>(),
|
||||
e => "" == e.TextContent);
|
||||
|
||||
Assert.Contains((LogLevel.Debug, "EndInvokeDispatchException"), logEvents);
|
||||
|
||||
await Client.ExpectCircuitErrorAndDisconnect(async () =>
|
||||
{
|
||||
await Assert.ThrowsAsync<TaskCanceledException>(() => Client.ClickAsync("event-handler-throw-sync", expectRenderBatch: true));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -329,8 +398,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
"false," +
|
||||
"\"There was an exception invoking \\u0027NotifyLocationChanged\\u0027 on assembly \\u0027Microsoft.AspNetCore.Components.Server\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]";
|
||||
|
||||
var (_, dotNetCompletions, batches) = ConfigureClient();
|
||||
await GoToTestComponent(batches);
|
||||
await GoToTestComponent(Batches);
|
||||
|
||||
// Act
|
||||
await Client.InvokeDotNetMethod(
|
||||
|
|
@ -341,8 +409,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
"[ \"invalidPayload\"}");
|
||||
|
||||
// Assert
|
||||
Assert.Single(dotNetCompletions, expectedError);
|
||||
await ValidateClientKeepsWorking(Client, batches);
|
||||
Assert.Single(DotNetCompletions, c => c.Message == expectedError);
|
||||
await ValidateClientKeepsWorking(Client, Batches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -353,8 +421,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
"false," +
|
||||
"\"There was an exception invoking \\u0027ReceiveTrivial\\u0027 on assembly \\u0027BasicTestApp\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]";
|
||||
|
||||
var (_, dotNetCompletions, batches) = ConfigureClient();
|
||||
await GoToTestComponent(batches);
|
||||
await GoToTestComponent(Batches);
|
||||
|
||||
// Act
|
||||
await Client.InvokeDotNetMethod(
|
||||
|
|
@ -365,62 +432,73 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
"[ { \"data\": {\"}} ]");
|
||||
|
||||
// Assert
|
||||
Assert.Single(dotNetCompletions, expectedError);
|
||||
await ValidateClientKeepsWorking(Client, batches);
|
||||
Assert.Single(DotNetCompletions, c => c.Message == expectedError);
|
||||
await ValidateClientKeepsWorking(Client, Batches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DispatchingEventsWithInvalidPayloadsDoesNotCrashTheCircuit()
|
||||
public async Task DispatchingEventsWithInvalidPayloadsShutsDownCircuitGracefully()
|
||||
{
|
||||
// Arrange
|
||||
var (interopCalls, dotNetCompletions, batches) = ConfigureClient();
|
||||
await GoToTestComponent(batches);
|
||||
await GoToTestComponent(Batches);
|
||||
var sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
|
||||
var logEvents = new List<(LogLevel logLevel, string)>();
|
||||
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name));
|
||||
|
||||
// Act
|
||||
await Client.HubConnection.InvokeAsync(
|
||||
await Client.ExpectCircuitError(async () =>
|
||||
{
|
||||
await Client.HubConnection.InvokeAsync(
|
||||
"DispatchBrowserEvent",
|
||||
null,
|
||||
null);
|
||||
});
|
||||
|
||||
Assert.Contains(
|
||||
(LogLevel.Debug, "DispatchEventFailedToParseEventData"),
|
||||
logEvents);
|
||||
|
||||
await ValidateClientKeepsWorking(Client, batches);
|
||||
// Taking any other action will fail because the circuit is disposed.
|
||||
await Client.ExpectCircuitErrorAndDisconnect(async () =>
|
||||
{
|
||||
await Assert.ThrowsAsync<TaskCanceledException>(() => Client.ClickAsync("event-handler-throw-sync", expectRenderBatch: true));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DispatchingEventsWithInvalidEventDescriptor()
|
||||
{
|
||||
// Arrange
|
||||
var (interopCalls, dotNetCompletions, batches) = ConfigureClient();
|
||||
await GoToTestComponent(batches);
|
||||
await GoToTestComponent(Batches);
|
||||
var sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
|
||||
var logEvents = new List<(LogLevel logLevel, string)>();
|
||||
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name));
|
||||
|
||||
// Act
|
||||
await Client.HubConnection.InvokeAsync(
|
||||
await Client.ExpectCircuitError(async () =>
|
||||
{
|
||||
await Client.HubConnection.InvokeAsync(
|
||||
"DispatchBrowserEvent",
|
||||
"{Invalid:{\"payload}",
|
||||
"{}");
|
||||
});
|
||||
|
||||
Assert.Contains(
|
||||
(LogLevel.Debug, "DispatchEventFailedToParseEventData"),
|
||||
logEvents);
|
||||
|
||||
await ValidateClientKeepsWorking(Client, batches);
|
||||
// Taking any other action will fail because the circuit is disposed.
|
||||
await Client.ExpectCircuitErrorAndDisconnect(async () =>
|
||||
{
|
||||
await Assert.ThrowsAsync<TaskCanceledException>(() => Client.ClickAsync("event-handler-throw-sync", expectRenderBatch: true));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DispatchingEventsWithInvalidEventArgs()
|
||||
{
|
||||
// Arrange
|
||||
var (interopCalls, dotNetCompletions, batches) = ConfigureClient();
|
||||
await GoToTestComponent(batches);
|
||||
await GoToTestComponent(Batches);
|
||||
var sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
|
||||
var logEvents = new List<(LogLevel logLevel, string)>();
|
||||
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name));
|
||||
|
|
@ -433,24 +511,30 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
EventArgsType = "mouse",
|
||||
};
|
||||
|
||||
await Client.HubConnection.InvokeAsync(
|
||||
"DispatchBrowserEvent",
|
||||
JsonSerializer.Serialize(browserDescriptor, TestJsonSerializerOptionsProvider.Options),
|
||||
"{Invalid:{\"payload}");
|
||||
await Client.ExpectCircuitError(async () =>
|
||||
{
|
||||
await Client.HubConnection.InvokeAsync(
|
||||
"DispatchBrowserEvent",
|
||||
JsonSerializer.Serialize(browserDescriptor, TestJsonSerializerOptionsProvider.Options),
|
||||
"{Invalid:{\"payload}");
|
||||
});
|
||||
|
||||
Assert.Contains(
|
||||
(LogLevel.Debug, "DispatchEventFailedToParseEventData"),
|
||||
logEvents);
|
||||
|
||||
await ValidateClientKeepsWorking(Client, batches);
|
||||
// Taking any other action will fail because the circuit is disposed.
|
||||
await Client.ExpectCircuitErrorAndDisconnect(async () =>
|
||||
{
|
||||
await Assert.ThrowsAsync<TaskCanceledException>(() => Client.ClickAsync("event-handler-throw-sync", expectRenderBatch: true));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/12940")]
|
||||
[Fact]
|
||||
public async Task DispatchingEventsWithInvalidEventHandlerId()
|
||||
{
|
||||
// Arrange
|
||||
var (interopCalls, dotNetCompletions, batches) = ConfigureClient();
|
||||
await GoToTestComponent(batches);
|
||||
await GoToTestComponent(Batches);
|
||||
var sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
|
||||
var logEvents = new List<(LogLevel logLevel, string eventIdName, Exception exception)>();
|
||||
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name, wc.Exception));
|
||||
|
|
@ -468,25 +552,31 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
EventArgsType = "mouse",
|
||||
};
|
||||
|
||||
await Client.HubConnection.InvokeAsync(
|
||||
await Client.ExpectCircuitError(async () =>
|
||||
{
|
||||
await Client.HubConnection.InvokeAsync(
|
||||
"DispatchBrowserEvent",
|
||||
JsonSerializer.Serialize(browserDescriptor, TestJsonSerializerOptionsProvider.Options),
|
||||
JsonSerializer.Serialize(mouseEventArgs, TestJsonSerializerOptionsProvider.Options));
|
||||
});
|
||||
|
||||
Assert.Contains(
|
||||
logEvents,
|
||||
e => e.eventIdName == "DispatchEventFailedToDispatchEvent" && e.logLevel == LogLevel.Debug &&
|
||||
e.exception is ArgumentException ae && ae.Message.Contains("There is no event handler with ID 1"));
|
||||
|
||||
await ValidateClientKeepsWorking(Client, batches);
|
||||
// Taking any other action will fail because the circuit is disposed.
|
||||
await Client.ExpectCircuitErrorAndDisconnect(async () =>
|
||||
{
|
||||
await Assert.ThrowsAsync<TaskCanceledException>(() => Client.ClickAsync("event-handler-throw-sync", expectRenderBatch: true));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EventHandlerThrowsSyncExceptionTerminatesTheCircuit()
|
||||
{
|
||||
// Arrange
|
||||
var (interopCalls, dotNetCompletions, batches) = ConfigureClient();
|
||||
await GoToTestComponent(batches);
|
||||
await GoToTestComponent(Batches);
|
||||
var sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
|
||||
var logEvents = new List<(LogLevel logLevel, string eventIdName, Exception exception)>();
|
||||
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name, wc.Exception));
|
||||
|
|
@ -496,12 +586,19 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
|
||||
Assert.Contains(
|
||||
logEvents,
|
||||
e => LogLevel.Warning == e.logLevel &&
|
||||
"UnhandledExceptionInCircuit" == e.eventIdName &&
|
||||
e => LogLevel.Error == e.logLevel &&
|
||||
"CircuitUnhandledException" == e.eventIdName &&
|
||||
"Handler threw an exception" == e.exception.Message);
|
||||
|
||||
// Now if you try to click again, you will get *forcibly* disconnected for trying to talk to
|
||||
// a circuit that's gone.
|
||||
await Client.ExpectCircuitErrorAndDisconnect(async () =>
|
||||
{
|
||||
await Assert.ThrowsAsync<TaskCanceledException>(() => Client.ClickAsync("event-handler-throw-sync", expectRenderBatch: true));
|
||||
});
|
||||
}
|
||||
|
||||
private Task ValidateClientKeepsWorking(BlazorClient Client, List<(int, byte[])> batches) =>
|
||||
private Task ValidateClientKeepsWorking(BlazorClient Client, IList<Batch> batches) =>
|
||||
ValidateClientKeepsWorking(Client, () => batches.Count);
|
||||
|
||||
private async Task ValidateClientKeepsWorking(BlazorClient Client, Func<int> countAccessor)
|
||||
|
|
@ -512,7 +609,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
Assert.Equal(currentBatches + 1, countAccessor());
|
||||
}
|
||||
|
||||
private async Task GoToTestComponent(List<(int, byte[])> batches)
|
||||
private async Task GoToTestComponent(IList<Batch> batches)
|
||||
{
|
||||
var rootUri = _serverFixture.RootUri;
|
||||
Assert.True(await Client.ConnectAsync(new Uri(rootUri, "/subdir"), prerendered: false), "Couldn't connect to the app");
|
||||
|
|
@ -522,15 +619,64 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
Assert.Equal(2, batches.Count);
|
||||
}
|
||||
|
||||
private (List<(int id, string identifier, string args)>, List<string>, List<(int, byte[])>) ConfigureClient()
|
||||
public void Dispose()
|
||||
{
|
||||
var interopCalls = new List<(int, string, string)>();
|
||||
Client.JSInterop += (int arg1, string arg2, string arg3) => interopCalls.Add((arg1, arg2, arg3));
|
||||
var batches = new List<(int, byte[])>();
|
||||
Client.RenderBatchReceived += (renderer, data) => batches.Add((renderer, data));
|
||||
var endInvokeDotNetCompletions = new List<string>();
|
||||
Client.DotNetInteropCompletion += (completion) => endInvokeDotNetCompletions.Add(completion);
|
||||
return (interopCalls, endInvokeDotNetCompletions, batches);
|
||||
TestSink.MessageLogged -= LogMessages;
|
||||
}
|
||||
|
||||
private class LogMessage
|
||||
{
|
||||
public LogMessage(LogLevel logLevel, string message, Exception exception)
|
||||
{
|
||||
LogLevel = logLevel;
|
||||
Message = message;
|
||||
Exception = exception;
|
||||
}
|
||||
|
||||
public LogLevel LogLevel { get; set; }
|
||||
public string Message { get; set; }
|
||||
public Exception Exception { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{LogLevel}: {Message}{(Exception != null ? Environment.NewLine : "")}{Exception}";
|
||||
}
|
||||
}
|
||||
|
||||
private class Batch
|
||||
{
|
||||
public Batch(int id, byte[] data)
|
||||
{
|
||||
Id = id;
|
||||
Data = data;
|
||||
}
|
||||
|
||||
public int Id { get; }
|
||||
public byte[] Data { get; }
|
||||
}
|
||||
|
||||
private class DotNetCompletion
|
||||
{
|
||||
public DotNetCompletion(string message)
|
||||
{
|
||||
Message = message;
|
||||
}
|
||||
|
||||
public string Message { get; }
|
||||
}
|
||||
|
||||
private class JSInteropCall
|
||||
{
|
||||
public JSInteropCall(int asyncHandle, string identifier, string argsJson)
|
||||
{
|
||||
AsyncHandle = asyncHandle;
|
||||
Identifier = identifier;
|
||||
ArgsJson = argsJson;
|
||||
}
|
||||
|
||||
public int AsyncHandle { get; }
|
||||
public string Identifier { get; }
|
||||
public string ArgsJson { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@
|
|||
<option value="BasicTestApp.MouseEventComponent">Mouse events</option>
|
||||
<option value="BasicTestApp.MovingCheckboxesComponent">Moving checkboxes diff case</option>
|
||||
<option value="BasicTestApp.MultipleChildContent">Multiple child content</option>
|
||||
<option value="BasicTestApp.NavigationFailureComponent">Navigation failure</option>
|
||||
<option value="BasicTestApp.ParentChildComponent">Parent component with child</option>
|
||||
<option value="BasicTestApp.PropertiesChangedHandlerParent">Parent component that changes parameters on child</option>
|
||||
<option value="BasicTestApp.RazorTemplates">Razor Templates</option>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
@implements IDisposable
|
||||
@inject NavigationManager Navigation
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
<p>
|
||||
This component is used to test the behaviour when you attach to NavigationManager.LocationChanged
|
||||
and throw an exception. We have a special code path to recognize this case and treat it as a failure
|
||||
in user code rather than invalid input.
|
||||
|
||||
This component is used headless tests for error handling. Markup that's provided here is for manually
|
||||
testing this case in the browser.
|
||||
</p>
|
||||
|
||||
<a href="test">Click here for some fireworks.</a>
|
||||
|
||||
@code {
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Navigation.LocationChanged += NavigationManager_LocationChanged;
|
||||
}
|
||||
|
||||
private void NavigationManager_LocationChanged(object sender, LocationChangedEventArgs e)
|
||||
{
|
||||
throw new InvalidTimeZoneException();
|
||||
}
|
||||
|
||||
void IDisposable.Dispose() => Navigation.LocationChanged -= NavigationManager_LocationChanged;
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
@using Microsoft.JSInterop
|
||||
@inject IJSRuntime JSRuntime
|
||||
@namespace BasicTestApp
|
||||
@using BasicTestApp.ServerReliability
|
||||
|
||||
<h1>Server reliability</h1>
|
||||
<p>
|
||||
This component is used on the server-side execution model to validate that the circuit is resilient to failures, intentional or not.
|
||||
|
|
@ -23,12 +25,68 @@
|
|||
|
||||
<button id="thecounter" @onclick="@IncrementCount">Click me</button>
|
||||
|
||||
<button id="constructor-throw" @onclick="@(() => showConstructorThrow = true)">Trigger exception on constructor</button>
|
||||
@if (showConstructorThrow)
|
||||
{
|
||||
<ThrowingConstructorComponent />
|
||||
}
|
||||
|
||||
<button id="attach-throw" @onclick="@(() => showAttachThrow = true)">Trigger exception on Attach</button>
|
||||
@if (showAttachThrow)
|
||||
{
|
||||
<ThrowingAttachComponent />
|
||||
}
|
||||
|
||||
<button id="setparameters-sync-throw" @onclick="@(() => showSetParametersSyncThrow = true)">Trigger exception synchronously on SetParametersAsync</button>
|
||||
@if (showSetParametersSyncThrow)
|
||||
{
|
||||
<ThrowingSetParametersSyncComponent />
|
||||
}
|
||||
|
||||
<button id="setparameters-async-throw" @onclick="@(() => showSetParametersAsyncThrow = true)">Trigger exception synchronously on SetParametersAsync</button>
|
||||
@if (showSetParametersAsyncThrow)
|
||||
{
|
||||
<ThrowingSetParametersAsyncComponent />
|
||||
}
|
||||
|
||||
<button id="render-throw" @onclick="@(() => showRenderThrow = true)">Trigger exception during rendering</button>
|
||||
@if (showRenderThrow)
|
||||
{
|
||||
<ThrowingRenderComponent />
|
||||
}
|
||||
|
||||
<button id="afterrender-sync-throw" @onclick="@(() => showOnAfterRenderSyncThrow = true)">Trigger exception synchronously on OnAfterRenderAsync</button>
|
||||
@if (showOnAfterRenderSyncThrow)
|
||||
{
|
||||
<ThrowingOnAfterRenderSyncComponent />
|
||||
}
|
||||
|
||||
<button id="afterrender-async-throw" @onclick="@(() => showOnAfterRenderAsyncThrow = true)">Trigger exception asynchronously on OnAfterRenderAsync</button>
|
||||
@if (showOnAfterRenderAsyncThrow)
|
||||
{
|
||||
<ThrowingOnAfterRenderAsyncComponent />
|
||||
}
|
||||
|
||||
<button id="dispose-throw" @onclick="@(() => showDisposeThrow = !showDisposeThrow)">Trigger exception during Dispose</button>
|
||||
@if (showDisposeThrow)
|
||||
{
|
||||
<ThrowingDisposeComponent />
|
||||
}
|
||||
|
||||
@code
|
||||
{
|
||||
int currentCount = 0;
|
||||
string errorMalformed = "";
|
||||
string errorSuccess = "";
|
||||
string errorFailure = "";
|
||||
bool showConstructorThrow;
|
||||
bool showAttachThrow;
|
||||
bool showSetParametersSyncThrow;
|
||||
bool showSetParametersAsyncThrow;
|
||||
bool showRenderThrow;
|
||||
bool showOnAfterRenderSyncThrow;
|
||||
bool showOnAfterRenderAsyncThrow;
|
||||
bool showDisposeThrow;
|
||||
|
||||
void IncrementCount()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace BasicTestApp.ServerReliability
|
||||
{
|
||||
public class ThrowingAttachComponent : IComponent
|
||||
{
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
throw new InvalidTimeZoneException();
|
||||
}
|
||||
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace BasicTestApp.ServerReliability
|
||||
{
|
||||
public class ThrowingConstructorComponent : IComponent
|
||||
{
|
||||
public ThrowingConstructorComponent()
|
||||
{
|
||||
throw new InvalidTimeZoneException();
|
||||
}
|
||||
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace BasicTestApp.ServerReliability
|
||||
{
|
||||
public class ThrowingDisposeComponent : IComponent, IDisposable
|
||||
{
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
renderHandle.Render(builder =>
|
||||
{
|
||||
// Do nothing.
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
throw new InvalidTimeZoneException();
|
||||
}
|
||||
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace BasicTestApp.ServerReliability
|
||||
{
|
||||
public class ThrowingOnAfterRenderAsyncComponent : IComponent, IHandleAfterRender
|
||||
{
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
renderHandle.Render(builder =>
|
||||
{
|
||||
// Do nothing.
|
||||
});
|
||||
}
|
||||
|
||||
public async Task OnAfterRenderAsync()
|
||||
{
|
||||
await Task.Yield();
|
||||
throw new InvalidTimeZoneException();
|
||||
}
|
||||
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace BasicTestApp.ServerReliability
|
||||
{
|
||||
public class ThrowingOnAfterRenderSyncComponent : IComponent, IHandleAfterRender
|
||||
{
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
renderHandle.Render(builder =>
|
||||
{
|
||||
// Do nothing.
|
||||
});
|
||||
}
|
||||
|
||||
public Task OnAfterRenderAsync()
|
||||
{
|
||||
throw new InvalidTimeZoneException();
|
||||
}
|
||||
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace BasicTestApp.ServerReliability
|
||||
{
|
||||
public class ThrowingRenderComponent : IComponent
|
||||
{
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
renderHandle.Render(builder =>
|
||||
{
|
||||
throw new InvalidTimeZoneException();
|
||||
});
|
||||
}
|
||||
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace BasicTestApp.ServerReliability
|
||||
{
|
||||
public class ThrowingSetParametersAsyncComponent : IComponent
|
||||
{
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
await Task.Yield();
|
||||
throw new InvalidTimeZoneException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace BasicTestApp.ServerReliability
|
||||
{
|
||||
public class ThrowingSetParametersSyncComponent : IComponent
|
||||
{
|
||||
public void Attach(RenderHandle renderHandle)
|
||||
{
|
||||
}
|
||||
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
throw new InvalidTimeZoneException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,12 +26,12 @@ namespace Ignitor
|
|||
{
|
||||
TaskCompletionSource.TrySetCanceled();
|
||||
});
|
||||
|
||||
ImplicitWait = DefaultLatencyTimeout != null;
|
||||
}
|
||||
|
||||
public TimeSpan? DefaultLatencyTimeout { get; set; } = TimeSpan.FromMilliseconds(500);
|
||||
|
||||
public Func<string, Exception> FormatError { get; set; }
|
||||
|
||||
private CancellationTokenSource CancellationTokenSource { get; }
|
||||
|
||||
private CancellationToken CancellationToken => CancellationTokenSource.Token;
|
||||
|
|
@ -42,10 +42,14 @@ namespace Ignitor
|
|||
|
||||
private CancellableOperation NextErrorReceived { get; set; }
|
||||
|
||||
private CancellableOperation NextDisconnect { get; set; }
|
||||
|
||||
private CancellableOperation NextJSInteropReceived { get; set; }
|
||||
|
||||
private CancellableOperation NextDotNetInteropCompletionReceived { get; set; }
|
||||
|
||||
public ILoggerProvider LoggerProvider { get; set; }
|
||||
|
||||
public bool ConfirmRenderBatch { get; set; } = true;
|
||||
|
||||
public event Action<int, string, string> JSInterop;
|
||||
|
|
@ -60,7 +64,7 @@ namespace Ignitor
|
|||
|
||||
public ElementHive Hive { get; set; } = new ElementHive();
|
||||
|
||||
public bool ImplicitWait { get; set; }
|
||||
public bool ImplicitWait => DefaultLatencyTimeout != null;
|
||||
|
||||
public HubConnection HubConnection { get; set; }
|
||||
|
||||
|
|
@ -112,6 +116,18 @@ namespace Ignitor
|
|||
return NextErrorReceived.Completion.Task;
|
||||
}
|
||||
|
||||
public Task PrepareForNextDisconnect(TimeSpan? timeout)
|
||||
{
|
||||
if (NextDisconnect?.Completion != null)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid state previous task not completed");
|
||||
}
|
||||
|
||||
NextDisconnect = new CancellableOperation(timeout);
|
||||
|
||||
return NextDisconnect.Completion.Task;
|
||||
}
|
||||
|
||||
public Task ClickAsync(string elementId, bool expectRenderBatch = true)
|
||||
{
|
||||
if (!Hive.TryFindElementById(elementId, out var elementNode))
|
||||
|
|
@ -128,14 +144,14 @@ namespace Ignitor
|
|||
}
|
||||
}
|
||||
|
||||
public async Task SelectAsync(string elementId, string value)
|
||||
public Task SelectAsync(string elementId, string value)
|
||||
{
|
||||
if (!Hive.TryFindElementById(elementId, out var elementNode))
|
||||
{
|
||||
throw new InvalidOperationException($"Could not find element with id {elementId}.");
|
||||
}
|
||||
|
||||
await ExpectRenderBatch(() => elementNode.SelectAsync(HubConnection, value));
|
||||
return ExpectRenderBatch(() => elementNode.SelectAsync(HubConnection, value));
|
||||
}
|
||||
|
||||
public async Task ExpectRenderBatch(Func<Task> action, TimeSpan? timeout = null)
|
||||
|
|
@ -166,6 +182,22 @@ namespace Ignitor
|
|||
await task;
|
||||
}
|
||||
|
||||
public async Task ExpectCircuitErrorAndDisconnect(Func<Task> action, TimeSpan? timeout = null)
|
||||
{
|
||||
// NOTE: timeout is used for each operation individually.
|
||||
await ExpectDisconnect(async () =>
|
||||
{
|
||||
await ExpectCircuitError(action, timeout);
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
public async Task ExpectDisconnect(Func<Task> action, TimeSpan? timeout = null)
|
||||
{
|
||||
var task = WaitForDisconnect(timeout);
|
||||
await action();
|
||||
await task;
|
||||
}
|
||||
|
||||
private Task WaitForRenderBatch(TimeSpan? timeout = null)
|
||||
{
|
||||
if (ImplicitWait)
|
||||
|
|
@ -220,12 +252,32 @@ namespace Ignitor
|
|||
}
|
||||
}
|
||||
|
||||
private async Task WaitForDisconnect(TimeSpan? timeout = null)
|
||||
{
|
||||
if (ImplicitWait)
|
||||
{
|
||||
if (DefaultLatencyTimeout == null && timeout == null)
|
||||
{
|
||||
throw new InvalidOperationException("Implicit wait without DefaultLatencyTimeout is not allowed.");
|
||||
}
|
||||
}
|
||||
|
||||
await PrepareForNextDisconnect(timeout ?? DefaultLatencyTimeout);
|
||||
}
|
||||
|
||||
public async Task<bool> ConnectAsync(Uri uri, bool prerendered, bool connectAutomatically = true)
|
||||
{
|
||||
var builder = new HubConnectionBuilder();
|
||||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IHubProtocol, IgnitorMessagePackHubProtocol>());
|
||||
builder.WithUrl(GetHubUrl(uri));
|
||||
builder.ConfigureLogging(l => l.AddConsole().SetMinimumLevel(LogLevel.Trace));
|
||||
builder.ConfigureLogging(l =>
|
||||
{
|
||||
l.SetMinimumLevel(LogLevel.Trace);
|
||||
if (LoggerProvider != null)
|
||||
{
|
||||
l.AddProvider(LoggerProvider);
|
||||
}
|
||||
});
|
||||
|
||||
HubConnection = builder.Build();
|
||||
await HubConnection.StartAsync(CancellationToken);
|
||||
|
|
@ -260,53 +312,32 @@ namespace Ignitor
|
|||
|
||||
private void OnEndInvokeDotNet(string completion)
|
||||
{
|
||||
try
|
||||
{
|
||||
DotNetInteropCompletion?.Invoke(completion);
|
||||
DotNetInteropCompletion?.Invoke(completion);
|
||||
|
||||
NextDotNetInteropCompletionReceived?.Completion?.TrySetResult(null);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
NextDotNetInteropCompletionReceived?.Completion?.TrySetException(e);
|
||||
}
|
||||
NextDotNetInteropCompletionReceived?.Completion?.TrySetResult(null);
|
||||
}
|
||||
|
||||
private void OnBeginInvokeJS(int asyncHandle, string identifier, string argsJson)
|
||||
{
|
||||
try
|
||||
{
|
||||
JSInterop?.Invoke(asyncHandle, identifier, argsJson);
|
||||
JSInterop?.Invoke(asyncHandle, identifier, argsJson);
|
||||
|
||||
NextJSInteropReceived?.Completion?.TrySetResult(null);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
NextJSInteropReceived?.Completion?.TrySetException(e);
|
||||
}
|
||||
NextJSInteropReceived?.Completion?.TrySetResult(null);
|
||||
}
|
||||
|
||||
private void OnRenderBatch(int batchId, byte[] batchData)
|
||||
{
|
||||
try
|
||||
RenderBatchReceived?.Invoke(batchId, batchData);
|
||||
|
||||
var batch = RenderBatchReader.Read(batchData);
|
||||
|
||||
Hive.Update(batch);
|
||||
|
||||
if (ConfirmRenderBatch)
|
||||
{
|
||||
RenderBatchReceived?.Invoke(batchId, batchData);
|
||||
|
||||
var batch = RenderBatchReader.Read(batchData);
|
||||
|
||||
Hive.Update(batch);
|
||||
|
||||
if (ConfirmRenderBatch)
|
||||
{
|
||||
_ = ConfirmBatch(batchId);
|
||||
}
|
||||
|
||||
NextBatchReceived?.Completion?.TrySetResult(null);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
NextBatchReceived?.Completion?.TrySetResult(e);
|
||||
_ = ConfirmBatch(batchId);
|
||||
}
|
||||
|
||||
NextBatchReceived?.Completion?.TrySetResult(null);
|
||||
}
|
||||
|
||||
public Task ConfirmBatch(int batchId, string error = null)
|
||||
|
|
@ -316,20 +347,19 @@ namespace Ignitor
|
|||
|
||||
private void OnError(string error)
|
||||
{
|
||||
try
|
||||
{
|
||||
OnCircuitError?.Invoke(error);
|
||||
OnCircuitError?.Invoke(error);
|
||||
|
||||
NextErrorReceived?.Completion?.TrySetResult(null);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
NextErrorReceived?.Completion?.TrySetResult(e);
|
||||
}
|
||||
var exception = FormatError?.Invoke(error) ?? new Exception(error);
|
||||
NextBatchReceived?.Completion?.TrySetException(exception);
|
||||
NextDotNetInteropCompletionReceived?.Completion.TrySetException(exception);
|
||||
NextJSInteropReceived?.Completion.TrySetException(exception);
|
||||
NextErrorReceived?.Completion?.TrySetResult(null);
|
||||
}
|
||||
|
||||
private Task OnClosedAsync(Exception ex)
|
||||
{
|
||||
NextDisconnect?.Completion?.TrySetResult(null);
|
||||
|
||||
if (ex == null)
|
||||
{
|
||||
TaskCompletionSource.TrySetResult(null);
|
||||
|
|
|
|||
Loading…
Reference in New Issue