Merge pull request #13136 from dotnet-maestro-bot/merge/release/3.0-to-master

[automated] Merge branch 'release/3.0' => 'master'
This commit is contained in:
Brennan 2019-08-15 08:28:49 -07:00 committed by GitHub
commit 5e575a3e64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 726 additions and 779 deletions

View File

@ -22,7 +22,7 @@ To run tests for the entire repo, run:
.\eng\scripts\TestHelix.ps1 .\eng\scripts\TestHelix.ps1
``` ```
This will restore, and then publish all of the test projects including some bootstrapping scripts that will install the correct dotnet runtime/sdk before running the test assemblies on the helix machine, and upload the job to helix, it won't wait for the jobs to complete, but you can go to https://mc.dot.net/#/user/$(your user name)/builds. This will restore, and then publish all of the test projects including some bootstrapping scripts that will install the correct dotnet runtime/sdk before running the test assemblies on the helix machine, and upload the job to helix.
## How do I look at the results of a helix run on Azure Pipelines? ## How do I look at the results of a helix run on Azure Pipelines?

View File

@ -171,8 +171,13 @@ try {
# Redirect stderr to stdout because PowerShell does not consistently handle output to stderr # Redirect stderr to stdout because PowerShell does not consistently handle output to stderr
$changedFiles = & cmd /c 'git --no-pager diff --ignore-space-at-eol --name-only 2>nul' $changedFiles = & cmd /c 'git --no-pager diff --ignore-space-at-eol --name-only 2>nul'
# Temporary: Disable check for blazor js file
$changedFilesExclusion = "src/Components/Web.JS/dist/Release/blazor.server.js"
if ($changedFiles) { if ($changedFiles) {
foreach ($file in $changedFiles) { foreach ($file in $changedFiles) {
if ($file -eq $changedFilesExclusion) {continue}
$filePath = Resolve-Path "${repoRoot}/${file}" $filePath = Resolve-Path "${repoRoot}/${file}"
LogError "Generated code is not up to date in $file. You might need to regenerate the reference assemblies or project list (see docs/ReferenceAssemblies.md and docs/ReferenceResolution.md)" -filepath $filePath LogError "Generated code is not up to date in $file. You might need to regenerate the reference assemblies or project list (see docs/ReferenceAssemblies.md and docs/ReferenceResolution.md)" -filepath $filePath
& git --no-pager diff --ignore-space-at-eol $filePath & git --no-pager diff --ignore-space-at-eol $filePath

View File

@ -11,7 +11,7 @@
<HelixAvailablePlatform Include="OSX" /> <HelixAvailablePlatform Include="OSX" />
<HelixAvailablePlatform Include="Linux" /> <HelixAvailablePlatform Include="Linux" />
</ItemGroup> </ItemGroup>
<!-- x64 queues --> <!-- x64 queues -->
<ItemGroup Condition="'$(IsWindowsOnlyTest)' != 'true' AND '$(TargetArchitecture)' == 'x64'"> <ItemGroup Condition="'$(IsWindowsOnlyTest)' != 'true' AND '$(TargetArchitecture)' == 'x64'">
<HelixAvailableTargetQueue Include="Windows.10.Amd64.ClientRS4.VS2017.Open" Platform="Windows" /> <HelixAvailableTargetQueue Include="Windows.10.Amd64.ClientRS4.VS2017.Open" Platform="Windows" />
@ -27,11 +27,11 @@
<HelixAvailableTargetQueue Include="Redhat.7.Amd64.Open" Platform="Linux" /> <HelixAvailableTargetQueue Include="Redhat.7.Amd64.Open" Platform="Linux" />
<HelixAvailableTargetQueue Include="(Fedora.28.Amd64.Open)Ubuntu.1604.Amd64.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:fedora-28-helix-09ca40b-20190508143249" Platform="Linux" /> <HelixAvailableTargetQueue Include="(Fedora.28.Amd64.Open)Ubuntu.1604.Amd64.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:fedora-28-helix-09ca40b-20190508143249" Platform="Linux" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(IsWindowsOnlyTest)' != 'true' AND '$(TargetArchitecture)' == 'arm64'"> <ItemGroup Condition="'$(IsWindowsOnlyTest)' != 'true' AND '$(TargetArchitecture)' == 'arm64'">
<!-- arm64 queues --> <!-- arm64 queues -->
<HelixAvailableTargetQueue Include="(Debian.9.Arm64.Open)Ubuntu.1604.Arm64.Docker.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-9-helix-arm64v8-0a0ebdd-20190312215438" Platform="Linux" /> <HelixAvailableTargetQueue Include="(Debian.9.Arm64.Open)Ubuntu.1604.Arm64.Docker.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:debian-9-helix-arm64v8-a12566d-20190807161036" Platform="Linux" />
<!-- Need to resolve permission issues on this docker queue <!-- Need to resolve permission issues on this docker queue
<HelixAvailableTargetQueue Include="(Alpine.38.Arm64)Ubuntu.1604.Arm64.Docker@mcr.microsoft.com/dotnet-buildtools/prereqs:alpine-3.8-helix-arm64v8-46e69dd-20190327215724" Platform="Linux" /> <HelixAvailableTargetQueue Include="(Alpine.38.Arm64)Ubuntu.1604.Arm64.Docker@mcr.microsoft.com/dotnet-buildtools/prereqs:alpine-3.8-helix-arm64v8-46e69dd-20190327215724" Platform="Linux" />
<HelixAvailableTargetQueue Include="(Ubuntu-1804.Arm64.Open)Ubuntu.1604.Arm64.Docker.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-18.04-helix-arm64v8-6f28fa9-20190606004102" Platform="Linux" /> <HelixAvailableTargetQueue Include="(Ubuntu-1804.Arm64.Open)Ubuntu.1604.Arm64.Docker.Open@mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-18.04-helix-arm64v8-6f28fa9-20190606004102" Platform="Linux" />

View File

@ -3,5 +3,6 @@
<NpmTestArgs>test</NpmTestArgs> <NpmTestArgs>test</NpmTestArgs>
<Configuration Condition="'$(Configuration)' == '' AND '$(ContinuousIntegrationBuild)' == 'true'">Release</Configuration> <Configuration Condition="'$(Configuration)' == '' AND '$(ContinuousIntegrationBuild)' == 'true'">Release</Configuration>
<Configuration Condition="'$(Configuration)' == ''">Debug</Configuration> <Configuration Condition="'$(Configuration)' == ''">Debug</Configuration>
<PackOnBuild>false</PackOnBuild>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@ -49,7 +49,9 @@
BuildInParallel="true" /> BuildInParallel="true" />
</Target> </Target>
<Target Name="Build" DependsOnTargets="$(BuildDependsOn)" /> <Target Name="Build" DependsOnTargets="$(BuildDependsOn)">
<CallTarget Targets="_Pack" Condition="'$(PackOnBuild)' == 'true'" />
</Target>
<Target Name="_Build" <Target Name="_Build"
Condition="'$(IsBuildable)' != 'false'" Condition="'$(IsBuildable)' != 'false'"
@ -66,11 +68,12 @@
</PackDependsOn> </PackDependsOn>
<PackDependsOn Condition="'$(NoBuild)' != 'true'"> <PackDependsOn Condition="'$(NoBuild)' != 'true'">
$(PackDependsOn); $(PackDependsOn);
Build Build;
_Pack
</PackDependsOn> </PackDependsOn>
</PropertyGroup> </PropertyGroup>
<Target Name="Pack" Condition="'$(IsPackable)' == 'true'" DependsOnTargets="$(PackDependsOn)"> <Target Name="_Pack" Condition="'$(IsPackable)' == 'true'" >
<PropertyGroup> <PropertyGroup>
<_PackageTargetPath>$(MSBuildProjectDirectory)\$(PackageFileName)</_PackageTargetPath> <_PackageTargetPath>$(MSBuildProjectDirectory)\$(PackageFileName)</_PackageTargetPath>
</PropertyGroup> </PropertyGroup>
@ -88,6 +91,8 @@
<OnError ExecuteTargets="_RestoreBackupPackageJsonFile" /> <OnError ExecuteTargets="_RestoreBackupPackageJsonFile" />
</Target> </Target>
<Target Name="Pack" Condition="'$(IsPackable)' == 'true'" DependsOnTargets="$(PackDependsOn)" />
<Target Name="_RestoreBackupPackageJsonFile"> <Target Name="_RestoreBackupPackageJsonFile">
<Move SourceFiles="$(_BackupPackageJson)" DestinationFiles="$(PackageJson)" /> <Move SourceFiles="$(_BackupPackageJson)" DestinationFiles="$(PackageJson)" />
</Target> </Target>

View File

@ -91,7 +91,6 @@ namespace Microsoft.AspNetCore.Blazor.Hosting
services.AddSingleton(_BrowserHostBuilderContext); services.AddSingleton(_BrowserHostBuilderContext);
services.AddSingleton<IWebAssemblyHost, WebAssemblyHost>(); services.AddSingleton<IWebAssemblyHost, WebAssemblyHost>();
services.AddSingleton<IJSRuntime>(WebAssemblyJSRuntime.Instance); services.AddSingleton<IJSRuntime>(WebAssemblyJSRuntime.Instance);
services.AddSingleton<IComponentContext, WebAssemblyComponentContext>();
services.AddSingleton<NavigationManager>(WebAssemblyNavigationManager.Instance); services.AddSingleton<NavigationManager>(WebAssemblyNavigationManager.Instance);
services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance); services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance);
services.AddSingleton<ILoggerFactory, WebAssemblyLoggerFactory>(); services.AddSingleton<ILoggerFactory, WebAssemblyLoggerFactory>();

View File

@ -1,12 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Components;
namespace Microsoft.AspNetCore.Blazor.Services
{
internal class WebAssemblyComponentContext : IComponentContext
{
public bool IsConnected => true;
}
}

View File

@ -1,16 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Services.Test
{
public class WebAssemblyComponentContextTest
{
[Fact]
public void IsConnected()
{
Assert.True(new WebAssemblyComponentContext().IsConnected);
}
}
}

View File

@ -97,6 +97,7 @@ namespace Microsoft.AspNetCore.Components
public abstract partial class Dispatcher public abstract partial class Dispatcher
{ {
protected Dispatcher() { } protected Dispatcher() { }
public void AssertAccess() { }
public abstract bool CheckAccess(); public abstract bool CheckAccess();
public static Microsoft.AspNetCore.Components.Dispatcher CreateDefault() { throw null; } public static Microsoft.AspNetCore.Components.Dispatcher CreateDefault() { throw null; }
public abstract System.Threading.Tasks.Task InvokeAsync(System.Action workItem); public abstract System.Threading.Tasks.Task InvokeAsync(System.Action workItem);
@ -196,10 +197,6 @@ namespace Microsoft.AspNetCore.Components
void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle); void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle);
System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters); System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters);
} }
public partial interface IComponentContext
{
bool IsConnected { get; }
}
public partial interface IHandleAfterRender public partial interface IHandleAfterRender
{ {
System.Threading.Tasks.Task OnAfterRenderAsync(); System.Threading.Tasks.Task OnAfterRenderAsync();

View File

@ -97,6 +97,7 @@ namespace Microsoft.AspNetCore.Components
public abstract partial class Dispatcher public abstract partial class Dispatcher
{ {
protected Dispatcher() { } protected Dispatcher() { }
public void AssertAccess() { }
public abstract bool CheckAccess(); public abstract bool CheckAccess();
public static Microsoft.AspNetCore.Components.Dispatcher CreateDefault() { throw null; } public static Microsoft.AspNetCore.Components.Dispatcher CreateDefault() { throw null; }
public abstract System.Threading.Tasks.Task InvokeAsync(System.Action workItem); public abstract System.Threading.Tasks.Task InvokeAsync(System.Action workItem);
@ -196,10 +197,6 @@ namespace Microsoft.AspNetCore.Components
void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle); void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle);
System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters); System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters);
} }
public partial interface IComponentContext
{
bool IsConnected { get; }
}
public partial interface IHandleAfterRender public partial interface IHandleAfterRender
{ {
System.Threading.Tasks.Task OnAfterRenderAsync(); System.Threading.Tasks.Task OnAfterRenderAsync();

View File

@ -23,6 +23,20 @@ namespace Microsoft.AspNetCore.Components
/// </summary> /// </summary>
internal event UnhandledExceptionEventHandler UnhandledException; internal event UnhandledExceptionEventHandler UnhandledException;
/// <summary>
/// Validates that the currently executing code is running inside the dispatcher.
/// </summary>
public void AssertAccess()
{
if (!CheckAccess())
{
throw new InvalidOperationException(
"The current thread is not associated with the Dispatcher. " +
"Use InvokeAsync() to switch execution to the Dispatcher when " +
"triggering rendering or component state.");
}
}
/// <summary> /// <summary>
/// Returns a value that determines whether using the dispatcher to invoke a work item is required /// Returns a value that determines whether using the dispatcher to invoke a work item is required
/// from the current context. /// from the current context.

View File

@ -1,19 +0,0 @@
// 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
{
/// <summary>
/// Provides information about the environment in which components are executing.
/// </summary>
public interface IComponentContext
{
/// <summary>
/// Gets a flag to indicate whether there is an active connection to the user's display.
/// </summary>
/// <example>During prerendering, the value will always be false.</example>
/// <example>During server-side execution, the value can be true or false depending on whether there is an active SignalR connection.</example>
/// <example>During client-side execution, the value will always be true.</example>
bool IsConnected { get; }
}
}

View File

@ -209,11 +209,11 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// </returns> /// </returns>
public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo fieldInfo, EventArgs eventArgs) public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo fieldInfo, EventArgs eventArgs)
{ {
EnsureSynchronizationContext(); Dispatcher.AssertAccess();
if (!_eventBindings.TryGetValue(eventHandlerId, out var callback)) if (!_eventBindings.TryGetValue(eventHandlerId, out var callback))
{ {
throw new ArgumentException($"There is no event handler with ID {eventHandlerId}"); throw new ArgumentException($"There is no event handler associated with this event. EventId: '{eventHandlerId}'.", nameof(eventHandlerId));
} }
Log.HandlingEvent(_logger, eventHandlerId, eventArgs); Log.HandlingEvent(_logger, eventHandlerId, eventArgs);
@ -337,7 +337,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// <param name="renderFragment">A <see cref="RenderFragment"/> that will supply the updated UI contents.</param> /// <param name="renderFragment">A <see cref="RenderFragment"/> that will supply the updated UI contents.</param>
internal void AddToRenderQueue(int componentId, RenderFragment renderFragment) internal void AddToRenderQueue(int componentId, RenderFragment renderFragment)
{ {
EnsureSynchronizationContext(); Dispatcher.AssertAccess();
var componentState = GetOptionalComponentState(componentId); var componentState = GetOptionalComponentState(componentId);
if (componentState == null) if (componentState == null)
@ -374,21 +374,6 @@ namespace Microsoft.AspNetCore.Components.Rendering
return eventHandlerId; return eventHandlerId;
} }
private void EnsureSynchronizationContext()
{
// Render operations are not thread-safe, so they need to be serialized by the dispatcher.
// Plus, any other logic that mutates state accessed during rendering also
// needs not to run concurrently with rendering so should be dispatched to
// the renderer's sync context.
if (!Dispatcher.CheckAccess())
{
throw new InvalidOperationException(
"The current thread is not associated with the Dispatcher. " +
"Use Invoke() or InvokeAsync() to switch execution to the Dispatcher when " +
"triggering rendering or modifying any state accessed during rendering.");
}
}
private ComponentState GetRequiredComponentState(int componentId) private ComponentState GetRequiredComponentState(int componentId)
=> _componentStateById.TryGetValue(componentId, out var componentState) => _componentStateById.TryGetValue(componentId, out var componentState)
? componentState ? componentState
@ -414,7 +399,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
private void ProcessRenderQueue() private void ProcessRenderQueue()
{ {
EnsureSynchronizationContext(); Dispatcher.AssertAccess();
if (_isBatchInProgress) if (_isBatchInProgress)
{ {

View File

@ -30,8 +30,6 @@ namespace Microsoft.AspNetCore.Components.Routing
[Inject] private INavigationInterception NavigationInterception { get; set; } [Inject] private INavigationInterception NavigationInterception { get; set; }
[Inject] private IComponentContext ComponentContext { get; set; }
[Inject] private ILoggerFactory LoggerFactory { get; set; } [Inject] private ILoggerFactory LoggerFactory { get; set; }
/// <summary> /// <summary>
@ -152,7 +150,7 @@ namespace Microsoft.AspNetCore.Components.Routing
Task IHandleAfterRender.OnAfterRenderAsync() Task IHandleAfterRender.OnAfterRenderAsync()
{ {
if (!_navigationInterceptionEnabled && ComponentContext.IsConnected) if (!_navigationInterceptionEnabled)
{ {
_navigationInterceptionEnabled = true; _navigationInterceptionEnabled = true;
return NavigationInterception.EnableNavigationInterceptionAsync(); return NavigationInterception.EnableNavigationInterceptionAsync();

View File

@ -2883,7 +2883,7 @@ namespace Microsoft.AspNetCore.Components.Test
{ {
return renderer.DispatchEventAsync(eventHandlerId, new EventArgs()); return renderer.DispatchEventAsync(eventHandlerId, new EventArgs());
}); });
Assert.Equal($"There is no event handler with ID {eventHandlerId}", ex.Message); Assert.Contains($"There is no event handler associated with this event. EventId: '{eventHandlerId}'.", ex.Message);
Assert.Equal(2, numEventsFired); Assert.Equal(2, numEventsFired);
} }

View File

@ -18,7 +18,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{ {
internal class CircuitHost : IAsyncDisposable internal class CircuitHost : IAsyncDisposable
{ {
private readonly SemaphoreSlim HandlerLock = new SemaphoreSlim(1);
private readonly IServiceScope _scope; private readonly IServiceScope _scope;
private readonly CircuitOptions _options; private readonly CircuitOptions _options;
private readonly CircuitHandler[] _circuitHandlers; private readonly CircuitHandler[] _circuitHandlers;
@ -27,24 +26,9 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
private bool _disposed; private bool _disposed;
/// <summary> /// <summary>
/// Sets the current <see cref="Circuits.Circuit"/>. /// Sets the <see cref="IJSRuntime"/> for the current excution context.
/// </summary> /// </summary>
/// <param name="circuitHost">The <see cref="Circuits.Circuit"/>.</param> public void SetCurrentJSRuntime() => JSInterop.JSRuntime.SetCurrentJSRuntime(JSRuntime);
/// <remarks>
/// Calling <see cref="SetCurrentCircuitHost(CircuitHost)"/> will store related values such as the
/// <see cref="IJSRuntime"/> and <see cref="Renderer"/>
/// in the local execution context. Application code should not need to call this method,
/// it is primarily used by the Server-Side Components infrastructure.
/// </remarks>
public static void SetCurrentCircuitHost(CircuitHost circuitHost)
{
if (circuitHost is null)
{
throw new ArgumentNullException(nameof(circuitHost));
}
JSInterop.JSRuntime.SetCurrentJSRuntime(circuitHost.JSRuntime);
}
// This event is fired when there's an unrecoverable exception coming from the circuit, and // 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 // it need so be torn down. The registry listens to this even so that the circuit can
@ -116,7 +100,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
try try
{ {
SetCurrentCircuitHost(this); SetCurrentJSRuntime();
_initialized = true; // We're ready to accept incoming JSInterop calls from here on _initialized = true; // We're ready to accept incoming JSInterop calls from here on
await OnCircuitOpenedAsync(cancellationToken); await OnCircuitOpenedAsync(cancellationToken);
@ -186,7 +170,18 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
try try
{ {
Renderer.Dispose(); Renderer.Dispose();
_scope.Dispose();
// This cast is needed because it's possible the scope may not support async dispose.
// Our DI container does, but other DI systems may not.
if (_scope is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync();
}
else
{
_scope.Dispose();
}
Log.DisposeSucceeded(_logger, CircuitId); Log.DisposeSucceeded(_logger, CircuitId);
} }
catch (Exception ex) catch (Exception ex)
@ -202,35 +197,28 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{ {
Log.CircuitOpened(_logger, Circuit.Id); Log.CircuitOpened(_logger, Circuit.Id);
await HandlerLock.WaitAsync(cancellationToken); Renderer.Dispatcher.AssertAccess();
try List<Exception> exceptions = null;
for (var i = 0; i < _circuitHandlers.Length; i++)
{ {
List<Exception> exceptions = null; var circuitHandler = _circuitHandlers[i];
try
for (var i = 0; i < _circuitHandlers.Length; i++)
{ {
var circuitHandler = _circuitHandlers[i]; await circuitHandler.OnCircuitOpenedAsync(Circuit, cancellationToken);
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)
{ {
throw new AggregateException("Encountered exceptions while executing circuit handlers.", exceptions); Log.CircuitHandlerFailed(_logger, circuitHandler, nameof(CircuitHandler.OnCircuitOpenedAsync), ex);
exceptions ??= new List<Exception>();
exceptions.Add(ex);
} }
} }
finally
if (exceptions != null)
{ {
HandlerLock.Release(); throw new AggregateException("Encountered exceptions while executing circuit handlers.", exceptions);
} }
} }
@ -238,35 +226,28 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{ {
Log.ConnectionUp(_logger, Circuit.Id, Client.ConnectionId); Log.ConnectionUp(_logger, Circuit.Id, Client.ConnectionId);
await HandlerLock.WaitAsync(cancellationToken); Renderer.Dispatcher.AssertAccess();
List<Exception> exceptions = null;
try for (var i = 0; i < _circuitHandlers.Length; i++)
{ {
List<Exception> exceptions = null; var circuitHandler = _circuitHandlers[i];
try
for (var i = 0; i < _circuitHandlers.Length; i++)
{ {
var circuitHandler = _circuitHandlers[i]; await circuitHandler.OnConnectionUpAsync(Circuit, cancellationToken);
try
{
await circuitHandler.OnConnectionUpAsync(Circuit, cancellationToken);
}
catch (Exception ex)
{
Log.CircuitHandlerFailed(_logger, circuitHandler, nameof(CircuitHandler.OnConnectionUpAsync), ex);
exceptions ??= new List<Exception>();
exceptions.Add(ex);
}
} }
catch (Exception ex)
if (exceptions != null)
{ {
throw new AggregateException("Encountered exceptions while executing circuit handlers.", exceptions); Log.CircuitHandlerFailed(_logger, circuitHandler, nameof(CircuitHandler.OnConnectionUpAsync), ex);
exceptions ??= new List<Exception>();
exceptions.Add(ex);
} }
} }
finally
if (exceptions != null)
{ {
HandlerLock.Release(); throw new AggregateException("Encountered exceptions while executing circuit handlers.", exceptions);
} }
} }
@ -274,35 +255,28 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{ {
Log.ConnectionDown(_logger, Circuit.Id, Client.ConnectionId); Log.ConnectionDown(_logger, Circuit.Id, Client.ConnectionId);
await HandlerLock.WaitAsync(cancellationToken); Renderer.Dispatcher.AssertAccess();
List<Exception> exceptions = null;
try for (var i = 0; i < _circuitHandlers.Length; i++)
{ {
List<Exception> exceptions = null; var circuitHandler = _circuitHandlers[i];
try
for (var i = 0; i < _circuitHandlers.Length; i++)
{ {
var circuitHandler = _circuitHandlers[i]; await circuitHandler.OnConnectionDownAsync(Circuit, cancellationToken);
try
{
await circuitHandler.OnConnectionDownAsync(Circuit, cancellationToken);
}
catch (Exception ex)
{
Log.CircuitHandlerFailed(_logger, circuitHandler, nameof(CircuitHandler.OnConnectionDownAsync), ex);
exceptions ??= new List<Exception>();
exceptions.Add(ex);
}
} }
catch (Exception ex)
if (exceptions != null)
{ {
throw new AggregateException("Encountered exceptions while executing circuit handlers.", exceptions); Log.CircuitHandlerFailed(_logger, circuitHandler, nameof(CircuitHandler.OnConnectionDownAsync), ex);
exceptions ??= new List<Exception>();
exceptions.Add(ex);
} }
} }
finally
if (exceptions != null)
{ {
HandlerLock.Release(); throw new AggregateException("Encountered exceptions while executing circuit handlers.", exceptions);
} }
} }
@ -310,35 +284,48 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{ {
Log.CircuitClosed(_logger, Circuit.Id); Log.CircuitClosed(_logger, Circuit.Id);
await HandlerLock.WaitAsync(cancellationToken); List<Exception> exceptions = null;
for (var i = 0; i < _circuitHandlers.Length; i++)
{
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);
}
}
if (exceptions != null)
{
throw new AggregateException("Encountered exceptions while executing circuit handlers.", exceptions);
}
}
// Called by the client when it completes rendering a batch.
// OnRenderCompletedAsync is used in a fire-and-forget context, so it's responsible for its own
// error handling.
public async Task OnRenderCompletedAsync(long renderId, string errorMessageOrNull)
{
AssertInitialized();
AssertNotDisposed();
try try
{ {
List<Exception> exceptions = null; _ = Renderer.OnRenderCompletedAsync(renderId, errorMessageOrNull);
for (var i = 0; i < _circuitHandlers.Length; i++)
{
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);
}
}
if (exceptions != null)
{
throw new AggregateException("Encountered exceptions while executing circuit handlers.", exceptions);
}
} }
finally catch (Exception e)
{ {
HandlerLock.Release(); // Captures sync exceptions when invoking OnRenderCompletedAsync.
// An exception might be throw synchronously when we receive an ack for a batch we never produced.
Log.OnRenderCompletedFailed(_logger, renderId, CircuitId, e);
await TryNotifyClientErrorAsync(Client, GetClientErrorMessage(e, $"Failed to complete render batch '{renderId}'."));
UnhandledException(this, new UnhandledExceptionEventArgs(e, isTerminating: false));
} }
} }
@ -353,7 +340,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{ {
await Renderer.Dispatcher.InvokeAsync(() => await Renderer.Dispatcher.InvokeAsync(() =>
{ {
SetCurrentCircuitHost(this); SetCurrentJSRuntime();
Log.BeginInvokeDotNet(_logger, callId, assemblyName, methodIdentifier, dotNetObjectId); Log.BeginInvokeDotNet(_logger, callId, assemblyName, methodIdentifier, dotNetObjectId);
DotNetDispatcher.BeginInvoke(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson); DotNetDispatcher.BeginInvoke(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson);
}); });
@ -363,10 +350,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
// We don't expect any of this code to actually throw, because DotNetDispatcher.BeginInvoke doesn't throw // 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. // however, we still want this to get logged if we do.
Log.BeginInvokeDotNetFailed(_logger, callId, assemblyName, methodIdentifier, dotNetObjectId, ex); Log.BeginInvokeDotNetFailed(_logger, callId, assemblyName, methodIdentifier, dotNetObjectId, ex);
if (Client.Connected) await TryNotifyClientErrorAsync(Client, GetClientErrorMessage(ex, "Interop call failed."));
{
await NotifyClientError(Client, "Interop call failed.");
}
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false)); UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false));
} }
} }
@ -382,7 +366,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{ {
await Renderer.Dispatcher.InvokeAsync(() => await Renderer.Dispatcher.InvokeAsync(() =>
{ {
SetCurrentCircuitHost(this); SetCurrentJSRuntime();
if (!succeded) if (!succeded)
{ {
// We can log the arguments here because it is simply the JS error with the call stack. // We can log the arguments here because it is simply the JS error with the call stack.
@ -401,10 +385,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
// An error completing JS interop means that the user sent invalid data, a well-behaved // An error completing JS interop means that the user sent invalid data, a well-behaved
// client won't do this. // client won't do this.
Log.EndInvokeDispatchException(_logger, ex); Log.EndInvokeDispatchException(_logger, ex);
if (Client.Connected) await TryNotifyClientErrorAsync(Client, GetClientErrorMessage(ex, "Invalid interop arguments."));
{
await NotifyClientError(Client, "Invalid interop arguments.");
}
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false)); UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false));
} }
} }
@ -419,17 +400,13 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
WebEventData webEventData; WebEventData webEventData;
try try
{ {
AssertInitialized();
webEventData = WebEventData.Parse(eventDescriptorJson, eventArgsJson); webEventData = WebEventData.Parse(eventDescriptorJson, eventArgsJson);
} }
catch (Exception ex) catch (Exception ex)
{ {
// Invalid event data is fatal. We expect a well-behaved client to send valid JSON. // Invalid event data is fatal. We expect a well-behaved client to send valid JSON.
Log.DispatchEventFailedToParseEventData(_logger, ex); Log.DispatchEventFailedToParseEventData(_logger, ex);
if (Client.Connected) await TryNotifyClientErrorAsync(Client, GetClientErrorMessage(ex, "Bad input data."));
{
await NotifyClientError(Client, "Invalid event data.");
}
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false)); UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false));
return; return;
} }
@ -438,7 +415,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{ {
await Renderer.Dispatcher.InvokeAsync(() => await Renderer.Dispatcher.InvokeAsync(() =>
{ {
SetCurrentCircuitHost(this); SetCurrentJSRuntime();
return Renderer.DispatchEventAsync( return Renderer.DispatchEventAsync(
webEventData.EventHandlerId, webEventData.EventHandlerId,
webEventData.EventFieldInfo, webEventData.EventFieldInfo,
@ -450,10 +427,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
// A failure in dispatching an event means that it was an attempt to use an invalid event id. // 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. // A well-behaved client won't do this.
Log.DispatchEventFailedToDispatchEvent(_logger, webEventData.EventHandlerId.ToString(), ex); Log.DispatchEventFailedToDispatchEvent(_logger, webEventData.EventHandlerId.ToString(), ex);
if (Client.Connected) await TryNotifyClientErrorAsync(Client, GetClientErrorMessage(ex, "Failed to dispatch event."));
{
await NotifyClientError(Client, "Failed to dispatch event.");
}
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false)); UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false));
} }
} }
@ -469,7 +443,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{ {
await Renderer.Dispatcher.InvokeAsync(() => await Renderer.Dispatcher.InvokeAsync(() =>
{ {
SetCurrentCircuitHost(this); SetCurrentJSRuntime();
Log.LocationChange(_logger, uri, CircuitId); Log.LocationChange(_logger, uri, CircuitId);
var navigationManager = (RemoteNavigationManager)Services.GetRequiredService<NavigationManager>(); var navigationManager = (RemoteNavigationManager)Services.GetRequiredService<NavigationManager>();
navigationManager.NotifyLocationChanged(uri, intercepted); navigationManager.NotifyLocationChanged(uri, intercepted);
@ -491,7 +465,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
// LocationChangeException means that it failed in user-code. Treat this like an unhandled // LocationChangeException means that it failed in user-code. Treat this like an unhandled
// exception in user-code. // exception in user-code.
Log.LocationChangeFailedInCircuit(_logger, uri, CircuitId, nex); Log.LocationChangeFailedInCircuit(_logger, uri, CircuitId, nex);
await ReportUnhandledException(nex); await TryNotifyClientErrorAsync(Client, GetClientErrorMessage(nex, "Location change failed."));
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(nex, isTerminating: false)); UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(nex, isTerminating: false));
} }
catch (Exception ex) catch (Exception ex)
@ -499,10 +473,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
// Any other exception means that it failed validation, or inside the NavigationManager. Treat // Any other exception means that it failed validation, or inside the NavigationManager. Treat
// this like bad data. // this like bad data.
Log.LocationChangeFailed(_logger, uri, CircuitId, ex); Log.LocationChangeFailed(_logger, uri, CircuitId, ex);
if (Client.Connected) await TryNotifyClientErrorAsync(Client, GetClientErrorMessage(ex, $"Location change to '{uri}' failed."));
{
await NotifyClientError(Client, $"Location change to {uri} failed.");
}
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false)); UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false));
} }
} }
@ -527,7 +498,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
// Dispatch any buffered renders we accumulated during a disconnect. // 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 // 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. // OnRenderCompletedAsync to be invoked to complete, and SignalR does not allow concurrent hub method invocations.
_ = Renderer.Dispatcher.InvokeAsync(() => Renderer.ProcessBufferedRenderBatches()); _ = Renderer.Dispatcher.InvokeAsync(() => Renderer.ProcessBufferedRenderBatches());
} }
@ -566,39 +537,50 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
private async Task ReportUnhandledException(Exception exception) private async Task ReportUnhandledException(Exception exception)
{ {
Log.CircuitUnhandledException(_logger, CircuitId, exception); Log.CircuitUnhandledException(_logger, CircuitId, exception);
await TryNotifyClientErrorAsync(Client, GetClientErrorMessage(exception), exception);
}
private string GetClientErrorMessage(Exception exception, string additionalInformation = null)
{
if (_options.DetailedErrors)
{
return exception.ToString();
}
else
{
return
$"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)}'. {additionalInformation}";
}
}
// exception is only populated when either the renderer or the synchronization context signal exceptions.
// In other cases it is null and should never be sent to the client.
// error contains the information to send to the client.
private async Task TryNotifyClientErrorAsync(IClientProxy client, string error, Exception exception = null)
{
if (!Client.Connected) if (!Client.Connected)
{ {
_logger.LogDebug("Client is disconnected YO."); Log.UnhandledExceptionClientDisconnected(
_logger,
CircuitId,
exception);
return; return;
} }
try try
{ {
if (_options.DetailedErrors) Log.CircuitTransmittingClientError(_logger, CircuitId);
{ await client.SendAsync("JS.Error", error);
await NotifyClientError(Client, exception.ToString()); Log.CircuitTransmittedClientErrorSuccess(_logger, CircuitId);
}
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) catch (Exception ex)
{ {
Log.CircuitUnhandledExceptionFailed(_logger, CircuitId, ex); Log.CircuitTransmitErrorFailed(_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 class Log
{ {
private static readonly Action<ILogger, Exception> _intializationStarted; private static readonly Action<ILogger, Exception> _intializationStarted;
@ -613,7 +595,10 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
private static readonly Action<ILogger, string, Exception> _onCircuitClosed; private static readonly Action<ILogger, string, Exception> _onCircuitClosed;
private static readonly Action<ILogger, Type, string, string, Exception> _circuitHandlerFailed; 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> _circuitUnhandledException;
private static readonly Action<ILogger, string, Exception> _circuitUnhandledExceptionFailed; private static readonly Action<ILogger, string, Exception> _circuitTransmittingClientError;
private static readonly Action<ILogger, string, Exception> _circuitTransmittedClientErrorSuccess;
private static readonly Action<ILogger, string, Exception> _circuitTransmitErrorFailed;
private static readonly Action<ILogger, string, Exception> _unhandledExceptionClientDisconnected;
private static readonly Action<ILogger, string, string, string, Exception> _beginInvokeDotNetStatic; 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, long, string, Exception> _beginInvokeDotNetInstance;
@ -628,6 +613,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
private static readonly Action<ILogger, string, string, Exception> _locationChangeSucceeded; 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> _locationChangeFailed;
private static readonly Action<ILogger, string, string, Exception> _locationChangeFailedInCircuit; private static readonly Action<ILogger, string, string, Exception> _locationChangeFailedInCircuit;
private static readonly Action<ILogger, long, string, Exception> _onRenderCompletedFailed;
private static class EventIds private static class EventIds
{ {
@ -644,7 +630,10 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
public static readonly EventId OnCircuitClosed = new EventId(109, "OnCircuitClosed"); public static readonly EventId OnCircuitClosed = new EventId(109, "OnCircuitClosed");
public static readonly EventId CircuitHandlerFailed = new EventId(110, "CircuitHandlerFailed"); public static readonly EventId CircuitHandlerFailed = new EventId(110, "CircuitHandlerFailed");
public static readonly EventId CircuitUnhandledException = new EventId(111, "CircuitUnhandledException"); public static readonly EventId CircuitUnhandledException = new EventId(111, "CircuitUnhandledException");
public static readonly EventId CircuitUnhandledExceptionFailed = new EventId(112, "CircuitUnhandledExceptionFailed"); public static readonly EventId CircuitTransmittingClientError = new EventId(112, "CircuitTransmittingClientError");
public static readonly EventId CircuitTransmittedClientErrorSuccess = new EventId(113, "CircuitTransmittedClientErrorSuccess");
public static readonly EventId CircuitTransmitErrorFailed = new EventId(114, "CircuitTransmitErrorFailed");
public static readonly EventId UnhandledExceptionClientDisconnected = new EventId(115, "UnhandledExceptionClientDisconnected");
// 200s used for interactive stuff // 200s used for interactive stuff
public static readonly EventId DispatchEventFailedToParseEventData = new EventId(200, "DispatchEventFailedToParseEventData"); public static readonly EventId DispatchEventFailedToParseEventData = new EventId(200, "DispatchEventFailedToParseEventData");
@ -659,6 +648,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
public static readonly EventId LocationChangeSucceded = new EventId(209, "LocationChangeSucceeded"); public static readonly EventId LocationChangeSucceded = new EventId(209, "LocationChangeSucceeded");
public static readonly EventId LocationChangeFailed = new EventId(210, "LocationChangeFailed"); public static readonly EventId LocationChangeFailed = new EventId(210, "LocationChangeFailed");
public static readonly EventId LocationChangeFailedInCircuit = new EventId(211, "LocationChangeFailedInCircuit"); public static readonly EventId LocationChangeFailedInCircuit = new EventId(211, "LocationChangeFailedInCircuit");
public static readonly EventId OnRenderCompletedFailed = new EventId(212, " OnRenderCompletedFailed");
} }
static Log() static Log()
@ -666,87 +656,102 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
_intializationStarted = LoggerMessage.Define( _intializationStarted = LoggerMessage.Define(
LogLevel.Debug, LogLevel.Debug,
EventIds.InitializationFailed, EventIds.InitializationFailed,
"Circuit initialization started"); "Circuit initialization started.");
_intializationSucceded = LoggerMessage.Define( _intializationSucceded = LoggerMessage.Define(
LogLevel.Debug, LogLevel.Debug,
EventIds.InitializationFailed, EventIds.InitializationFailed,
"Circuit initialization succeeded"); "Circuit initialization succeeded.");
_intializationFailed = LoggerMessage.Define( _intializationFailed = LoggerMessage.Define(
LogLevel.Debug, LogLevel.Debug,
EventIds.InitializationFailed, EventIds.InitializationFailed,
"Circuit initialization failed"); "Circuit initialization failed.");
_disposeStarted = LoggerMessage.Define<string>( _disposeStarted = LoggerMessage.Define<string>(
LogLevel.Debug, LogLevel.Debug,
EventIds.DisposeStarted, EventIds.DisposeStarted,
"Disposing circuit {CircuitId} started"); "Disposing circuit '{CircuitId}' started.");
_disposeSucceded = LoggerMessage.Define<string>( _disposeSucceded = LoggerMessage.Define<string>(
LogLevel.Debug, LogLevel.Debug,
EventIds.DisposeSucceeded, EventIds.DisposeSucceeded,
"Disposing circuit {CircuitId} succeded"); "Disposing circuit '{CircuitId}' succeded.");
_disposeFailed = LoggerMessage.Define<string>( _disposeFailed = LoggerMessage.Define<string>(
LogLevel.Debug, LogLevel.Debug,
EventIds.DisposeFailed, EventIds.DisposeFailed,
"Disposing circuit {CircuitId} failed"); "Disposing circuit '{CircuitId}' failed.");
_onCircuitOpened = LoggerMessage.Define<string>( _onCircuitOpened = LoggerMessage.Define<string>(
LogLevel.Debug, LogLevel.Debug,
EventIds.OnCircuitOpened, EventIds.OnCircuitOpened,
"Opening circuit with id {CircuitId}."); "Opening circuit with id '{CircuitId}'.");
_onConnectionUp = LoggerMessage.Define<string, string>( _onConnectionUp = LoggerMessage.Define<string, string>(
LogLevel.Debug, LogLevel.Debug,
EventIds.OnConnectionUp, EventIds.OnConnectionUp,
"Circuit id {CircuitId} connected using connection {ConnectionId}."); "Circuit id '{CircuitId}' connected using connection '{ConnectionId}'.");
_onConnectionDown = LoggerMessage.Define<string, string>( _onConnectionDown = LoggerMessage.Define<string, string>(
LogLevel.Debug, LogLevel.Debug,
EventIds.OnConnectionDown, EventIds.OnConnectionDown,
"Circuit id {CircuitId} disconnected from connection {ConnectionId}."); "Circuit id '{CircuitId}' disconnected from connection '{ConnectionId}'.");
_onCircuitClosed = LoggerMessage.Define<string>( _onCircuitClosed = LoggerMessage.Define<string>(
LogLevel.Debug, LogLevel.Debug,
EventIds.OnCircuitClosed, EventIds.OnCircuitClosed,
"Closing circuit with id {CircuitId}."); "Closing circuit with id '{CircuitId}'.");
_circuitHandlerFailed = LoggerMessage.Define<Type, string, string>( _circuitHandlerFailed = LoggerMessage.Define<Type, string, string>(
LogLevel.Error, LogLevel.Error,
EventIds.CircuitHandlerFailed, EventIds.CircuitHandlerFailed,
"Unhandled error invoking circuit handler type {handlerType}.{handlerMethod}: {Message}"); "Unhandled error invoking circuit handler type {handlerType}.{handlerMethod}: {Message}");
_circuitUnhandledException = LoggerMessage.Define<string>( _circuitUnhandledException = LoggerMessage.Define<string>(
LogLevel.Error, LogLevel.Error,
EventIds.CircuitUnhandledException, EventIds.CircuitUnhandledException,
"Unhandled exception in circuit {CircuitId}"); "Unhandled exception in circuit '{CircuitId}'.");
_circuitUnhandledExceptionFailed = LoggerMessage.Define<string>( _circuitTransmittingClientError = LoggerMessage.Define<string>(
LogLevel.Debug, LogLevel.Debug,
EventIds.CircuitUnhandledExceptionFailed, EventIds.CircuitTransmittingClientError,
"Failed to transmit exception to client in circuit {CircuitId}"); "About to notify client of an error in circuit '{CircuitId}'.");
_circuitTransmittedClientErrorSuccess = LoggerMessage.Define<string>(
LogLevel.Debug,
EventIds.CircuitTransmittedClientErrorSuccess,
"Successfully transmitted error to client in circuit '{CircuitId}'.");
_circuitTransmitErrorFailed = LoggerMessage.Define<string>(
LogLevel.Debug,
EventIds.CircuitTransmitErrorFailed,
"Failed to transmit exception to client in circuit '{CircuitId}'.");
_unhandledExceptionClientDisconnected = LoggerMessage.Define<string>(
LogLevel.Debug,
EventIds.UnhandledExceptionClientDisconnected,
"An exception ocurred on the circuit host '{CircuitId}' while the client is disconnected.");
_beginInvokeDotNetStatic = LoggerMessage.Define<string, string, string>( _beginInvokeDotNetStatic = LoggerMessage.Define<string, string, string>(
LogLevel.Debug, LogLevel.Debug,
EventIds.BeginInvokeDotNet, EventIds.BeginInvokeDotNet,
"Invoking static method with identifier '{MethodIdentifier}' on assembly '{Assembly}' with callback id '{CallId}'"); "Invoking static method with identifier '{MethodIdentifier}' on assembly '{Assembly}' with callback id '{CallId}'.");
_beginInvokeDotNetInstance = LoggerMessage.Define<string, long, string>( _beginInvokeDotNetInstance = LoggerMessage.Define<string, long, string>(
LogLevel.Debug, LogLevel.Debug,
EventIds.BeginInvokeDotNet, EventIds.BeginInvokeDotNet,
"Invoking instance method '{MethodIdentifier}' on instance '{DotNetObjectId}' with callback id '{CallId}'"); "Invoking instance method '{MethodIdentifier}' on instance '{DotNetObjectId}' with callback id '{CallId}'.");
_beginInvokeDotNetStaticFailed = LoggerMessage.Define<string, string, string>( _beginInvokeDotNetStaticFailed = LoggerMessage.Define<string, string, string>(
LogLevel.Debug, LogLevel.Debug,
EventIds.BeginInvokeDotNetFailed, EventIds.BeginInvokeDotNetFailed,
"Failed to invoke static method with identifier '{MethodIdentifier}' on assembly '{Assembly}' with callback id '{CallId}'"); "Failed to invoke static method with identifier '{MethodIdentifier}' on assembly '{Assembly}' with callback id '{CallId}'.");
_beginInvokeDotNetInstanceFailed = LoggerMessage.Define<string, long, string>( _beginInvokeDotNetInstanceFailed = LoggerMessage.Define<string, long, string>(
LogLevel.Debug, LogLevel.Debug,
EventIds.BeginInvokeDotNetFailed, EventIds.BeginInvokeDotNetFailed,
"Failed to invoke instance method '{MethodIdentifier}' on instance '{DotNetObjectId}' with callback id '{CallId}'"); "Failed to invoke instance method '{MethodIdentifier}' on instance '{DotNetObjectId}' with callback id '{CallId}'.");
_endInvokeDispatchException = LoggerMessage.Define( _endInvokeDispatchException = LoggerMessage.Define(
LogLevel.Debug, LogLevel.Debug,
@ -776,25 +781,30 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
_locationChange = LoggerMessage.Define<string, string>( _locationChange = LoggerMessage.Define<string, string>(
LogLevel.Debug, LogLevel.Debug,
EventIds.LocationChange, EventIds.LocationChange,
"Location changing to {URI} in circuit {CircuitId}."); "Location changing to {URI} in circuit '{CircuitId}'.");
_locationChangeSucceeded = LoggerMessage.Define<string, string>( _locationChangeSucceeded = LoggerMessage.Define<string, string>(
LogLevel.Debug, LogLevel.Debug,
EventIds.LocationChangeSucceded, EventIds.LocationChangeSucceded,
"Location change to {URI} in circuit {CircuitId} succeded."); "Location change to '{URI}' in circuit '{CircuitId}' succeded.");
_locationChangeFailed = LoggerMessage.Define<string, string>( _locationChangeFailed = LoggerMessage.Define<string, string>(
LogLevel.Debug, LogLevel.Debug,
EventIds.LocationChangeFailed, EventIds.LocationChangeFailed,
"Location change to {URI} in circuit {CircuitId} failed."); "Location change to '{URI}' in circuit '{CircuitId}' failed.");
_locationChangeFailedInCircuit = LoggerMessage.Define<string, string>( _locationChangeFailedInCircuit = LoggerMessage.Define<string, string>(
LogLevel.Error, LogLevel.Error,
EventIds.LocationChangeFailed, EventIds.LocationChangeFailed,
"Location change to {URI} in circuit {CircuitId} failed."); "Location change to '{URI}' in circuit '{CircuitId}' failed.");
_onRenderCompletedFailed = LoggerMessage.Define<long, string>(
LogLevel.Debug,
EventIds.OnRenderCompletedFailed,
"Failed to complete render batch '{RenderId}' in circuit host '{CircuitId}'.");
} }
public static void InitializationStarted(ILogger logger) =>_intializationStarted(logger, null); public static void InitializationStarted(ILogger logger) => _intializationStarted(logger, null);
public static void InitializationSucceeded(ILogger logger) => _intializationSucceded(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 InitializationFailed(ILogger logger, Exception exception) => _intializationFailed(logger, exception);
public static void DisposeStarted(ILogger logger, string circuitId) => _disposeStarted(logger, circuitId, null); public static void DisposeStarted(ILogger logger, string circuitId) => _disposeStarted(logger, circuitId, null);
@ -802,7 +812,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
public static void DisposeFailed(ILogger logger, string circuitId, Exception exception) => _disposeFailed(logger, circuitId, exception); 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 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 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 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 CircuitClosed(ILogger logger, string circuitId) => _onCircuitClosed(logger, circuitId, null);
public static void CircuitHandlerFailed(ILogger logger, CircuitHandler handler, string handlerMethod, Exception exception) public static void CircuitHandlerFailed(ILogger logger, CircuitHandler handler, string handlerMethod, Exception exception)
@ -816,7 +826,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
} }
public static void CircuitUnhandledException(ILogger logger, string circuitId, Exception exception) => _circuitUnhandledException(logger, circuitId, exception); 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 CircuitTransmitErrorFailed(ILogger logger, string circuitId, Exception exception) => _circuitTransmitErrorFailed(logger, circuitId, exception);
public static void EndInvokeDispatchException(ILogger logger, Exception ex) => _endInvokeDispatchException(logger, ex); 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 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 EndInvokeJSSucceeded(ILogger logger, long asyncCall) => _endInvokeJSSucceeded(logger, asyncCall, null);
@ -851,6 +861,10 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
public static void LocationChangeSucceeded(ILogger logger, string uri, string circuitId) => _locationChangeSucceeded(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 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); public static void LocationChangeFailedInCircuit(ILogger logger, string uri, string circuitId, Exception exception) => _locationChangeFailedInCircuit(logger, uri, circuitId, exception);
public static void UnhandledExceptionClientDisconnected(ILogger logger, string circuitId, Exception exception) => _unhandledExceptionClientDisconnected(logger, circuitId, exception);
public static void CircuitTransmittingClientError(ILogger logger, string circuitId) => _circuitTransmittingClientError(logger, circuitId, null);
public static void CircuitTransmittedClientErrorSuccess(ILogger logger, string circuitId) => _circuitTransmittedClientErrorSuccess(logger, circuitId, null);
public static void OnRenderCompletedFailed(ILogger logger, long renderId, string circuitId, Exception e) => _onRenderCompletedFailed(logger, renderId, circuitId, e);
} }
} }
} }

View File

@ -51,9 +51,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
var scope = _scopeFactory.CreateScope(); var scope = _scopeFactory.CreateScope();
var jsRuntime = (RemoteJSRuntime)scope.ServiceProvider.GetRequiredService<IJSRuntime>(); var jsRuntime = (RemoteJSRuntime)scope.ServiceProvider.GetRequiredService<IJSRuntime>();
var componentContext = (RemoteComponentContext)scope.ServiceProvider.GetRequiredService<IComponentContext>();
jsRuntime.Initialize(client); jsRuntime.Initialize(client);
componentContext.Initialize(client);
var navigationManager = (RemoteNavigationManager)scope.ServiceProvider.GetRequiredService<NavigationManager>(); var navigationManager = (RemoteNavigationManager)scope.ServiceProvider.GetRequiredService<NavigationManager>();
var navigationInterception = (RemoteNavigationInterception)scope.ServiceProvider.GetRequiredService<INavigationInterception>(); var navigationInterception = (RemoteNavigationInterception)scope.ServiceProvider.GetRequiredService<INavigationInterception>();

View File

@ -1,19 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Components.Server.Circuits
{
internal class RemoteComponentContext : IComponentContext
{
private CircuitClientProxy _clientProxy;
public bool IsConnected => _clientProxy != null && _clientProxy.Connected;
internal void Initialize(CircuitClientProxy clientProxy)
{
_clientProxy = clientProxy ?? throw new ArgumentNullException(nameof(clientProxy));
}
}
}

View File

@ -222,7 +222,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
// disposed. // disposed.
} }
public Task OnRenderCompleted(long incomingBatchId, string errorMessageOrNull) public Task OnRenderCompletedAsync(long incomingBatchId, string errorMessageOrNull)
{ {
if (_disposing) if (_disposing)
{ {
@ -273,9 +273,9 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
if (lastBatchId < incomingBatchId) if (lastBatchId < incomingBatchId)
{ {
HandleException( // This exception is due to a bad client input, so we mark it as such to prevent loging it as a warning and
new InvalidOperationException($"Received an acknowledgement for batch with id '{incomingBatchId}' when the last batch produced was '{lastBatchId}'.")); // flooding the logs with warnings.
return Task.CompletedTask; throw new InvalidOperationException($"Received an acknowledgement for batch with id '{incomingBatchId}' when the last batch produced was '{lastBatchId}'.");
} }
// Normally we will not have pending renders, but it might happen that we reached the limit of // Normally we will not have pending renders, but it might happen that we reached the limit of
@ -320,7 +320,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
} }
else else
{ {
pendingRenderInfo.TrySetException(new RemoteRendererException(errorMessageOrNull)); pendingRenderInfo.TrySetException(new InvalidOperationException(errorMessageOrNull));
} }
} }

View File

@ -1,21 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Components.Web.Rendering
{
/// <summary>
/// Represents an exception related to remote rendering.
/// </summary>
internal class RemoteRendererException : Exception
{
/// <summary>
/// Constructs an instance of <see cref="RemoteRendererException"/>.
/// </summary>
/// <param name="message">The exception message.</param>
public RemoteRendererException(string message) : base(message)
{
}
}
}

View File

@ -6,8 +6,6 @@ using System.Runtime.CompilerServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Server.Circuits; using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -203,7 +201,7 @@ namespace Microsoft.AspNetCore.Components.Server
} }
Log.ReceivedConfirmationForBatch(_logger, renderId); Log.ReceivedConfirmationForBatch(_logger, renderId);
_ = circuitHost.Renderer.OnRenderCompleted(renderId, errorMessageOrNull); _ = circuitHost.OnRenderCompletedAsync(renderId, errorMessageOrNull);
} }
public async ValueTask OnLocationChanged(string uri, bool intercepted) public async ValueTask OnLocationChanged(string uri, bool intercepted)
@ -281,29 +279,15 @@ namespace Microsoft.AspNetCore.Components.Server
private static readonly Action<ILogger, string, Exception> _circuitHostShutdown = 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"); 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");
private static readonly Action<ILogger, string, Exception> _invalidInputData = private static readonly Action<ILogger, string, Exception> _invalidInputData =
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(8, "InvalidInputData"), "Call to '{CallSite}' received invalid input data"); LoggerMessage.Define<string>(LogLevel.Debug, new EventId(7, "InvalidInputData"), "Call to '{CallSite}' received invalid input data");
private static readonly Action<ILogger, Exception> _circuitInitializationFailed = private static readonly Action<ILogger, Exception> _circuitInitializationFailed =
LoggerMessage.Define(LogLevel.Debug, new EventId(9, "CircuitInitializationFailed"), "Circuit initialization failed"); LoggerMessage.Define(LogLevel.Debug, new EventId(8, "CircuitInitializationFailed"), "Circuit initialization failed");
public static void NoComponentsRegisteredInEndpoint(ILogger logger, string endpointDisplayName) public static void NoComponentsRegisteredInEndpoint(ILogger logger, string endpointDisplayName) => _noComponentsRegisteredInEndpoint(logger, endpointDisplayName, null);
{
_noComponentsRegisteredInEndpoint(logger, endpointDisplayName, null);
}
public static void ReceivedConfirmationForBatch(ILogger logger, long batchId) public static void ReceivedConfirmationForBatch(ILogger logger, long batchId) => _receivedConfirmationForBatch(logger, batchId, null);
{
_receivedConfirmationForBatch(logger, batchId, null);
}
public static void UnhandledExceptionInCircuit(ILogger logger, string circuitId, Exception exception)
{
_unhandledExceptionInCircuit(logger, circuitId, exception);
}
public static void CircuitAlreadyInitialized(ILogger logger, string circuitId) => _circuitAlreadyInitialized(logger, circuitId, null); public static void CircuitAlreadyInitialized(ILogger logger, string circuitId) => _circuitAlreadyInitialized(logger, circuitId, null);
@ -311,8 +295,6 @@ namespace Microsoft.AspNetCore.Components.Server
public static void CircuitHostShutdown(ILogger logger, [CallerMemberName] string callSite = "") => _circuitHostShutdown(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); public static void InvalidInputData(ILogger logger, [CallerMemberName] string callSite = "") => _invalidInputData(logger, callSite, null);
public static void CircuitInitializationFailed(ILogger logger, Exception exception) => _circuitInitializationFailed(logger, exception); public static void CircuitInitializationFailed(ILogger logger, Exception exception) => _circuitInitializationFailed(logger, exception);

View File

@ -70,7 +70,6 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<NavigationManager, RemoteNavigationManager>(); services.AddScoped<NavigationManager, RemoteNavigationManager>();
services.AddScoped<IJSRuntime, RemoteJSRuntime>(); services.AddScoped<IJSRuntime, RemoteJSRuntime>();
services.AddScoped<INavigationInterception, RemoteNavigationInterception>(); services.AddScoped<INavigationInterception, RemoteNavigationInterception>();
services.AddScoped<IComponentContext, RemoteComponentContext>();
services.AddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>(); services.AddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>();
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<CircuitOptions>, CircuitOptionsJSInteropDetailedErrorsConfiguration>()); services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<CircuitOptions>, CircuitOptionsJSInteropDetailedErrorsConfiguration>());

View File

@ -39,6 +39,33 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
Assert.Null(circuitHost.Handle.CircuitHost); Assert.Null(circuitHost.Handle.CircuitHost);
} }
[Fact]
public async Task DisposeAsync_DisposesScopeAsynchronouslyIfPossible()
{
// Arrange
var serviceScope = new Mock<IServiceScope>();
serviceScope
.As<IAsyncDisposable>()
.Setup(f => f.DisposeAsync())
.Returns(new ValueTask(Task.CompletedTask))
.Verifiable();
var remoteRenderer = GetRemoteRenderer();
var circuitHost = TestCircuitHost.Create(
Guid.NewGuid().ToString(),
serviceScope.Object,
remoteRenderer);
// Act
await circuitHost.DisposeAsync();
// Assert
serviceScope.Verify(s => s.Dispose(), Times.Never());
serviceScope.As<IAsyncDisposable>().Verify(s => s.DisposeAsync(), Times.Once());
Assert.True(remoteRenderer.Disposed);
Assert.Null(circuitHost.Handle.CircuitHost);
}
[Fact] [Fact]
public async Task DisposeAsync_DisposesResourcesAndSilencesException() public async Task DisposeAsync_DisposesResourcesAndSilencesException()
{ {

View File

@ -270,46 +270,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
Assert.False(registry.DisconnectedCircuits.TryGetValue(circuitHost.CircuitId, out _)); Assert.False(registry.DisconnectedCircuits.TryGetValue(circuitHost.CircuitId, out _));
} }
[Fact]
public async Task Connect_WhileDisconnectIsInProgress_SeriallyExecutesCircuitHandlers()
{
// Arrange
var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory();
var registry = new TestCircuitRegistry(circuitIdFactory);
registry.BeforeDisconnect = new ManualResetEventSlim();
// This verifies that connection up \ down events on a circuit handler are always invoked serially.
var circuitHandler = new SerialCircuitHandler();
var tcs = new TaskCompletionSource<int>();
var circuitHost = TestCircuitHost.Create(circuitIdFactory.CreateCircuitId(), handlers: new[] { circuitHandler });
registry.Register(circuitHost);
var client = Mock.Of<IClientProxy>();
var newId = "new-connection";
// Act
var disconnect = Task.Run(() =>
{
var task = registry.DisconnectAsync(circuitHost, circuitHost.Client.ConnectionId);
tcs.SetResult(0);
return task;
});
var connect = Task.Run(async () =>
{
registry.BeforeDisconnect.Set();
await tcs.Task;
await registry.ConnectAsync(circuitHost.CircuitId, client, newId, default);
});
await Task.WhenAll(disconnect, connect);
// Assert
Assert.Single(registry.ConnectedCircuits.Values);
Assert.False(registry.DisconnectedCircuits.TryGetValue(circuitHost.CircuitId, out _));
Assert.True(circuitHandler.OnConnectionDownExecuted, "OnConnectionDownAsync should have been executed.");
Assert.True(circuitHandler.OnConnectionUpExecuted, "OnConnectionUpAsync should have been executed.");
}
[Fact] [Fact]
public async Task DisconnectWhenAConnectIsInProgress() public async Task DisconnectWhenAConnectIsInProgress()
{ {
@ -444,37 +404,5 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
NullLogger<CircuitRegistry>.Instance, NullLogger<CircuitRegistry>.Instance,
factory ?? TestCircuitIdFactory.CreateTestFactory()); factory ?? TestCircuitIdFactory.CreateTestFactory());
} }
private class SerialCircuitHandler : CircuitHandler
{
private readonly SemaphoreSlim _sempahore = new SemaphoreSlim(1);
public bool OnConnectionUpExecuted { get; private set; }
public bool OnConnectionDownExecuted { get; private set; }
public override async Task OnConnectionUpAsync(Circuit circuit, CancellationToken cancellationToken)
{
Assert.True(await _sempahore.WaitAsync(0), "This should be serialized and consequently without contention");
await Task.Delay(10);
Assert.False(OnConnectionUpExecuted);
Assert.True(OnConnectionDownExecuted);
OnConnectionUpExecuted = true;
_sempahore.Release();
}
public override async Task OnConnectionDownAsync(Circuit circuit, CancellationToken cancellationToken)
{
Assert.True(await _sempahore.WaitAsync(0), "This should be serialized and consequently without contention");
await Task.Delay(10);
Assert.False(OnConnectionUpExecuted);
Assert.False(OnConnectionDownExecuted);
OnConnectionDownExecuted = true;
_sempahore.Release();
}
}
} }
} }

View File

@ -1,46 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.AspNetCore.SignalR;
using Xunit;
namespace Microsoft.AspNetCore.Components.Web.Rendering
{
public class RemoteComponentContextTest
{
[Fact]
public void IfNotInitialized_IsConnectedReturnsFalse()
{
Assert.False(new RemoteComponentContext().IsConnected);
}
[Fact]
public void IfInitialized_IsConnectedValueDeterminedByCircuitProxy()
{
// Arrange
var clientProxy = new FakeClientProxy();
var circuitProxy = new CircuitClientProxy(clientProxy, "test connection");
var remoteComponentContext = new RemoteComponentContext();
// Act/Assert: Can observe connected state
remoteComponentContext.Initialize(circuitProxy);
Assert.True(remoteComponentContext.IsConnected);
// Act/Assert: Can observe disconnected state
circuitProxy.SetDisconnected();
Assert.False(remoteComponentContext.IsConnected);
}
private class FakeClientProxy : IClientProxy
{
public Task SendCoreAsync(string method, object[] args, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
}
}
}

View File

@ -89,7 +89,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
component.TriggerRender(); component.TriggerRender();
} }
await renderer.OnRenderCompleted(2, null); await renderer.OnRenderCompletedAsync(2, null);
// Assert // Assert
Assert.Equal(9, renderer._unacknowledgedRenderBatches.Count); Assert.Equal(9, renderer._unacknowledgedRenderBatches.Count);
@ -115,7 +115,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
} }
Assert.Equal(10, renderer._unacknowledgedRenderBatches.Count); Assert.Equal(10, renderer._unacknowledgedRenderBatches.Count);
await renderer.OnRenderCompleted(2, null); await renderer.OnRenderCompletedAsync(2, null);
// Assert // Assert
Assert.Equal(10, renderer._unacknowledgedRenderBatches.Count); Assert.Equal(10, renderer._unacknowledgedRenderBatches.Count);
@ -153,7 +153,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
var componentId = renderer.AssignRootComponentId(component); var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender(); component.TriggerRender();
_ = renderer.OnRenderCompleted(2, null); _ = renderer.OnRenderCompletedAsync(2, null);
@event.Reset(); @event.Reset();
firstBatchTCS.SetResult(null); firstBatchTCS.SetResult(null);
@ -171,7 +171,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
foreach (var id in renderIds.ToArray()) foreach (var id in renderIds.ToArray())
{ {
_ = renderer.OnRenderCompleted(id, null); _ = renderer.OnRenderCompletedAsync(id, null);
} }
secondBatchTCS.SetResult(null); secondBatchTCS.SetResult(null);
@ -234,14 +234,14 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
}; };
// Receive the ack for the intial batch // Receive the ack for the intial batch
_ = renderer.OnRenderCompleted(2, null); _ = renderer.OnRenderCompletedAsync(2, null);
// Receive the ack for the second batch // Receive the ack for the second batch
_ = renderer.OnRenderCompleted(3, null); _ = renderer.OnRenderCompletedAsync(3, null);
firstBatchTCS.SetResult(null); firstBatchTCS.SetResult(null);
secondBatchTCS.SetResult(null); secondBatchTCS.SetResult(null);
// Repeat the ack for the third batch // Repeat the ack for the third batch
_ = renderer.OnRenderCompleted(3, null); _ = renderer.OnRenderCompletedAsync(3, null);
// Assert // Assert
Assert.Empty(exceptions); Assert.Empty(exceptions);
@ -297,14 +297,14 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
}; };
// Receive the ack for the intial batch // Receive the ack for the intial batch
_ = renderer.OnRenderCompleted(2, null); _ = renderer.OnRenderCompletedAsync(2, null);
// Receive the ack for the second batch // Receive the ack for the second batch
_ = renderer.OnRenderCompleted(2, null); _ = renderer.OnRenderCompletedAsync(2, null);
firstBatchTCS.SetResult(null); firstBatchTCS.SetResult(null);
secondBatchTCS.SetResult(null); secondBatchTCS.SetResult(null);
// Repeat the ack for the third batch // Repeat the ack for the third batch
_ = renderer.OnRenderCompleted(3, null); _ = renderer.OnRenderCompletedAsync(3, null);
// Assert // Assert
Assert.Empty(exceptions); Assert.Empty(exceptions);
@ -358,7 +358,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
}; };
// Pretend that we missed the ack for the initial batch // Pretend that we missed the ack for the initial batch
_ = renderer.OnRenderCompleted(3, null); _ = renderer.OnRenderCompletedAsync(3, null);
firstBatchTCS.SetResult(null); firstBatchTCS.SetResult(null);
secondBatchTCS.SetResult(null); secondBatchTCS.SetResult(null);
@ -414,12 +414,11 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
exceptions.Add(e); exceptions.Add(e);
}; };
_ = renderer.OnRenderCompleted(4, null); var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => renderer.OnRenderCompletedAsync(4, null));
firstBatchTCS.SetResult(null); firstBatchTCS.SetResult(null);
secondBatchTCS.SetResult(null); secondBatchTCS.SetResult(null);
// Assert // Assert
var exception = Assert.Single(exceptions);
Assert.Equal( Assert.Equal(
"Received an acknowledgement for batch with id '4' when the last batch produced was '3'.", "Received an acknowledgement for batch with id '4' when the last batch produced was '3'.",
exception.Message); exception.Message);

View File

@ -11,11 +11,20 @@ namespace Microsoft.AspNetCore.Components.Web
{ {
// This class represents the second half of parsing incoming event data, // This class represents the second half of parsing incoming event data,
// once the type of the eventArgs becomes known. // once the type of the eventArgs becomes known.
public static WebEventData Parse(string eventDescriptorJson, string eventArgsJson) public static WebEventData Parse(string eventDescriptorJson, string eventArgsJson)
{ {
WebEventDescriptor eventDescriptor;
try
{
eventDescriptor = Deserialize<WebEventDescriptor>(eventDescriptorJson);
}
catch (Exception e)
{
throw new InvalidOperationException("Error parsing the event descriptor", e);
}
return Parse( return Parse(
Deserialize<WebEventDescriptor>(eventDescriptorJson), eventDescriptor,
eventArgsJson); eventArgsJson);
} }
@ -25,7 +34,7 @@ namespace Microsoft.AspNetCore.Components.Web
eventDescriptor.BrowserRendererId, eventDescriptor.BrowserRendererId,
eventDescriptor.EventHandlerId, eventDescriptor.EventHandlerId,
InterpretEventFieldInfo(eventDescriptor.EventFieldInfo), InterpretEventFieldInfo(eventDescriptor.EventFieldInfo),
ParseEventArgsJson(eventDescriptor.EventArgsType, eventArgsJson)); ParseEventArgsJson(eventDescriptor.EventHandlerId, eventDescriptor.EventArgsType, eventArgsJson));
} }
private WebEventData(int browserRendererId, ulong eventHandlerId, EventFieldInfo eventFieldInfo, EventArgs eventArgs) private WebEventData(int browserRendererId, ulong eventHandlerId, EventFieldInfo eventFieldInfo, EventArgs eventArgs)
@ -44,43 +53,34 @@ namespace Microsoft.AspNetCore.Components.Web
public EventArgs EventArgs { get; } public EventArgs EventArgs { get; }
private static EventArgs ParseEventArgsJson(string eventArgsType, string eventArgsJson) private static EventArgs ParseEventArgsJson(ulong eventHandlerId, string eventArgsType, string eventArgsJson)
{ {
switch (eventArgsType) try
{ {
case "change": return eventArgsType switch
return DeserializeChangeEventArgs(eventArgsJson); {
case "clipboard": "change" => DeserializeChangeEventArgs(eventArgsJson),
return Deserialize<UIClipboardEventArgs>(eventArgsJson); "clipboard" => Deserialize<UIClipboardEventArgs>(eventArgsJson),
case "drag": "drag" => Deserialize<UIDragEventArgs>(eventArgsJson),
return Deserialize<UIDragEventArgs>(eventArgsJson); "error" => Deserialize<UIErrorEventArgs>(eventArgsJson),
case "error": "focus" => Deserialize<UIFocusEventArgs>(eventArgsJson),
return Deserialize<UIErrorEventArgs>(eventArgsJson); "keyboard" => Deserialize<UIKeyboardEventArgs>(eventArgsJson),
case "focus": "mouse" => Deserialize<UIMouseEventArgs>(eventArgsJson),
return Deserialize<UIFocusEventArgs>(eventArgsJson); "pointer" => Deserialize<UIPointerEventArgs>(eventArgsJson),
case "keyboard": "progress" => Deserialize<UIProgressEventArgs>(eventArgsJson),
return Deserialize<UIKeyboardEventArgs>(eventArgsJson); "touch" => Deserialize<UITouchEventArgs>(eventArgsJson),
case "mouse": "unknown" => EventArgs.Empty,
return Deserialize<UIMouseEventArgs>(eventArgsJson); "wheel" => Deserialize<UIWheelEventArgs>(eventArgsJson),
case "pointer": _ => throw new InvalidOperationException($"Unsupported event type '{eventArgsType}'. EventId: '{eventHandlerId}'."),
return Deserialize<UIPointerEventArgs>(eventArgsJson); };
case "progress": }
return Deserialize<UIProgressEventArgs>(eventArgsJson); catch (Exception e)
case "touch": {
return Deserialize<UITouchEventArgs>(eventArgsJson); throw new InvalidOperationException($"There was an error parsing the event arguments. EventId: '{eventHandlerId}'.", e);
case "unknown":
return EventArgs.Empty;
case "wheel":
return Deserialize<UIWheelEventArgs>(eventArgsJson);
default:
throw new ArgumentException($"Unsupported value '{eventArgsType}'.", nameof(eventArgsType));
} }
} }
private static T Deserialize<T>(string json) private static T Deserialize<T>(string json) => JsonSerializer.Deserialize<T>(json, JsonSerializerOptionsProvider.Options);
{
return JsonSerializer.Deserialize<T>(json, JsonSerializerOptionsProvider.Options);
}
private static EventFieldInfo InterpretEventFieldInfo(EventFieldInfo fieldInfo) private static EventFieldInfo InterpretEventFieldInfo(EventFieldInfo fieldInfo)
{ {

File diff suppressed because one or more lines are too long

View File

@ -37,8 +37,13 @@ async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
} }
const reconnection = existingConnection || await initializeConnection(options, logger); const reconnection = existingConnection || await initializeConnection(options, logger);
if (reconnection.state !== signalR.HubConnectionState.Connected) {
logger.log(LogLevel.Information, 'Reconnection attempt failed. Unable to connect to the server.');
return false;
}
if (!(await circuit.reconnect(reconnection))) { if (!(await circuit.reconnect(reconnection))) {
logger.log(LogLevel.Information, 'Reconnection attempt failed.'); logger.log(LogLevel.Information, 'Reconnection attempt to the circuit failed.');
return false; return false;
} }

View File

@ -31,12 +31,18 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
]; ];
this.modal.style.cssText = modalStyles.join(';'); this.modal.style.cssText = modalStyles.join(';');
this.modal.innerHTML = '<h5 style="margin-top: 20px"></h5><button style="margin:5px auto 5px">Retry?</button><p>Alternatively, <a href>reload</a></p>'; this.modal.innerHTML = '<h5 style="margin-top: 20px"></h5><button style="margin:5px auto 5px">Retry</button><p>Alternatively, <a href>reload</a></p>';
this.message = this.modal.querySelector('h5')!; this.message = this.modal.querySelector('h5')!;
this.button = this.modal.querySelector('button')!; this.button = this.modal.querySelector('button')!;
this.reloadParagraph = this.modal.querySelector('p')!; this.reloadParagraph = this.modal.querySelector('p')!;
this.button.addEventListener('click', () => window['Blazor'].reconnect()); this.button.addEventListener('click', async () => {
this.show();
const successful = await window['Blazor'].reconnect();
if (!successful) {
this.failed();
}
});
this.reloadParagraph.querySelector('a')!.addEventListener('click', () => location.reload()); this.reloadParagraph.querySelector('a')!.addEventListener('click', () => location.reload());
} }
@ -57,7 +63,7 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
failed(): void { failed(): void {
this.button.style.display = 'block'; this.button.style.display = 'block';
this.reloadParagraph.style.display = 'block'; this.reloadParagraph.style.display = 'none';
this.message.textContent = 'Failed to reconnect to the server.'; this.message.innerHTML = 'Reconnection failed. Try <a href>reloading</a> the page if you\'re unable to reconnect.';
} }
} }

View File

@ -45,7 +45,7 @@ describe('DefaultReconnectDisplay', () => {
display.failed(); display.failed();
expect(display.modal.style.display).toBe('block'); expect(display.modal.style.display).toBe('block');
expect(display.message.textContent).toBe('Failed to reconnect to the server.'); expect(display.message.innerHTML).toBe('Reconnection failed. Try <a href=\"\">reloading</a> the page if you\'re unable to reconnect.');
expect(display.button.style.display).toBe('block'); expect(display.button.style.display).toBe('block');
}); });

View File

@ -58,24 +58,23 @@ describe('DefaultReconnectionHandler', () => {
expect(reconnect).toHaveBeenCalledTimes(1); expect(reconnect).toHaveBeenCalledTimes(1);
}); });
// Skipped while under investigation: https://github.com/aspnet/AspNetCore/issues/12578 it('invokes failed if reconnect fails', async () => {
// it('invokes failed if reconnect fails', async () => { const testDisplay = createTestDisplay();
// const testDisplay = createTestDisplay(); const reconnect = jest.fn().mockRejectedValue(null);
// const reconnect = jest.fn().mockRejectedValue(null); const handler = new DefaultReconnectionHandler(NullLogger.instance, testDisplay, reconnect);
// const handler = new DefaultReconnectionHandler(NullLogger.instance, testDisplay, reconnect); window.console.error = jest.fn();
// window.console.error = jest.fn();
// handler.onConnectionDown({ handler.onConnectionDown({
// maxRetries: 3, maxRetries: 2,
// retryIntervalMilliseconds: 20, retryIntervalMilliseconds: 5,
// dialogId: 'ignored' dialogId: 'ignored'
// }); });
// await delay(500); await delay(500);
// expect(testDisplay.show).toHaveBeenCalled(); expect(testDisplay.show).toHaveBeenCalled();
// expect(testDisplay.failed).toHaveBeenCalled(); expect(testDisplay.failed).toHaveBeenCalled();
// expect(reconnect).toHaveBeenCalledTimes(3); expect(reconnect).toHaveBeenCalledTimes(2);
// }); });
}); });
function attachUserSpecifiedUI(options: ReconnectionOptions): Element { function attachUserSpecifiedUI(options: ReconnectionOptions): Element {

View File

@ -6,9 +6,12 @@ using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data; using System.Data;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Diagnostics;
using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ignitor; using Ignitor;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.SignalR.Client; using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -20,7 +23,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{ {
public class ComponentHubReliabilityTest : IClassFixture<AspNetSiteServerFixture>, IDisposable public class ComponentHubReliabilityTest : IClassFixture<AspNetSiteServerFixture>, IDisposable
{ {
private static readonly TimeSpan DefaultLatencyTimeout = TimeSpan.FromSeconds(10); private static readonly TimeSpan DefaultLatencyTimeout = TimeSpan.FromSeconds(Debugger.IsAttached ? 60 : 10);
private readonly AspNetSiteServerFixture _serverFixture; private readonly AspNetSiteServerFixture _serverFixture;
public ComponentHubReliabilityTest(AspNetSiteServerFixture serverFixture, ITestOutputHelper output) public ComponentHubReliabilityTest(AspNetSiteServerFixture serverFixture, ITestOutputHelper output)
@ -52,7 +55,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
return new Exception(error + Environment.NewLine + logs); return new Exception(error + Environment.NewLine + logs);
}; };
_ = _serverFixture.RootUri; // this is needed for the side-effects of getting the URI. _ = _serverFixture.RootUri; // this is needed for the side-effects of getting the URI.
TestSink = _serverFixture.Host.Services.GetRequiredService<TestSink>(); TestSink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
TestSink.MessageLogged += LogMessages; TestSink.MessageLogged += LogMessages;
} }
@ -199,6 +202,118 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
Assert.Contains(Logs, l => (l.LogLevel, l.Message) == (LogLevel.Debug, "Call to 'DispatchBrowserEvent' received before the circuit host initialization")); Assert.Contains(Logs, l => (l.LogLevel, l.Message) == (LogLevel.Debug, "Call to 'DispatchBrowserEvent' received before the circuit host initialization"));
} }
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");
Assert.Single(batches);
await Client.SelectAsync("test-selector-select", "BasicTestApp.CounterComponent");
Assert.Equal(2, batches.Count);
}
[Fact]
public async Task DispatchingAnInvalidEventArgument_DoesNotProduceWarnings()
{
// Arrange
var expectedError = $"There was an unhandled exception on the current circuit, so this circuit will be terminated. For more details turn on " +
$"detailed exceptions in 'CircuitOptions.DetailedErrors'. Bad input data.";
var eventDescriptor = Serialize(new WebEventDescriptor()
{
BrowserRendererId = 0,
EventHandlerId = 3,
EventArgsType = "mouse",
});
await GoToTestComponent(Batches);
Assert.Equal(2, Batches.Count);
// Act
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
"DispatchBrowserEvent",
eventDescriptor,
"{sadfadsf]"));
// Assert
var actualError = Assert.Single(Errors);
Assert.Equal(expectedError, actualError);
Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information);
Assert.Contains(Logs, l => (l.LogLevel, l.Exception?.Message) == (LogLevel.Debug, "There was an error parsing the event arguments. EventId: '3'."));
}
[Fact]
public async Task DispatchingAnInvalidEvent_DoesNotTriggerWarnings()
{
// Arrange
var expectedError = $"There was an unhandled exception on the current circuit, so this circuit will be terminated. For more details turn on " +
$"detailed exceptions in 'CircuitOptions.DetailedErrors'. Failed to dispatch event.";
var eventDescriptor = Serialize(new WebEventDescriptor()
{
BrowserRendererId = 0,
EventHandlerId = 1990,
EventArgsType = "mouse",
});
var eventArgs = new UIMouseEventArgs
{
Type = "click",
Detail = 1,
ScreenX = 47,
ScreenY = 258,
ClientX = 47,
ClientY = 155,
};
await GoToTestComponent(Batches);
Assert.Equal(2, Batches.Count);
// Act
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
"DispatchBrowserEvent",
eventDescriptor,
Serialize(eventArgs)));
// Assert
var actualError = Assert.Single(Errors);
Assert.Equal(expectedError, actualError);
Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information);
Assert.Contains(Logs, l => (l.LogLevel, l.Message, l.Exception?.Message) ==
(LogLevel.Debug,
"There was an error dispatching the event '1990' to the application.",
"There is no event handler associated with this event. EventId: '1990'. (Parameter 'eventHandlerId')"));
}
[Fact]
public async Task DispatchingAnInvalidRenderAcknowledgement_DoesNotTriggerWarnings()
{
// Arrange
var expectedError = $"There was an unhandled exception on the current circuit, so this circuit will be terminated. For more details turn on " +
$"detailed exceptions in 'CircuitOptions.DetailedErrors'. Failed to complete render batch '1846'.";
await GoToTestComponent(Batches);
Assert.Equal(2, Batches.Count);
Client.ConfirmRenderBatch = false;
await Client.ClickAsync("counter");
// Act
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
"OnRenderCompleted",
1846,
null));
// Assert
var actualError = Assert.Single(Errors);
Assert.Equal(expectedError, actualError);
Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information);
Assert.Contains(Logs, l => (l.LogLevel, l.Message, l.Exception?.Message) ==
(LogLevel.Debug,
$"Failed to complete render batch '1846' in circuit host '{Client.CircuitId}'.",
"Received an acknowledgement for batch with id '1846' when the last batch produced was '4'."));
}
[Fact] [Fact]
public async Task CannotInvokeOnRenderCompletedBeforeInitialization() public async Task CannotInvokeOnRenderCompletedBeforeInitialization()
{ {
@ -249,7 +364,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
public async Task OnLocationChanged_ReportsDebugForExceptionInValidation() public async Task OnLocationChanged_ReportsDebugForExceptionInValidation()
{ {
// Arrange // Arrange
var expectedError = "Location change to http://example.com failed."; var expectedError = "There was an unhandled exception on the current circuit, so this circuit will be terminated. " +
"For more details turn on detailed exceptions in 'CircuitOptions.DetailedErrors'. " +
"Location change to 'http://example.com' failed.";
var rootUri = _serverFixture.RootUri; var rootUri = _serverFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir"); var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri, prerendered: false), "Couldn't connect to the app"); Assert.True(await Client.ConnectAsync(baseUri, prerendered: false), "Couldn't connect to the app");
@ -267,7 +385,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information); Assert.DoesNotContain(Logs, l => l.LogLevel > LogLevel.Information);
Assert.Contains(Logs, l => Assert.Contains(Logs, l =>
{ {
return l.LogLevel == LogLevel.Debug && Regex.IsMatch(l.Message, "Location change to http://example.com in circuit .* failed."); return (l.LogLevel, l.Message) == (LogLevel.Debug, $"Location change to 'http://example.com' in circuit '{Client.CircuitId}' failed.");
}); });
} }
@ -275,7 +393,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
public async Task OnLocationChanged_ReportsErrorForExceptionInUserCode() public async Task OnLocationChanged_ReportsErrorForExceptionInUserCode()
{ {
// Arrange // Arrange
var expectedError = "There was an unhandled exception .?"; var expectedError = "There was an unhandled exception on the current circuit, so this circuit will be terminated. " +
"For more details turn on detailed exceptions in 'CircuitOptions.DetailedErrors'. " +
"Location change failed.";
var rootUri = _serverFixture.RootUri; var rootUri = _serverFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir"); var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri, prerendered: false), "Couldn't connect to the app"); Assert.True(await Client.ConnectAsync(baseUri, prerendered: false), "Couldn't connect to the app");
@ -287,14 +408,14 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync( await Client.ExpectCircuitError(() => Client.HubConnection.SendAsync(
"OnLocationChanged", "OnLocationChanged",
new Uri(baseUri, "/test").AbsoluteUri, new Uri(baseUri, "/test").AbsoluteUri,
false)); false));
// Assert // Assert
var actualError = Assert.Single(Errors); var actualError = Assert.Single(Errors);
Assert.Matches(expectedError, actualError); Assert.Equal(expectedError, actualError);
Assert.Contains(Logs, l => Assert.Contains(Logs, l =>
{ {
return l.LogLevel == LogLevel.Error && Regex.IsMatch(l.Message, "Unhandled exception in circuit .*"); return (l.LogLevel, l.Message) == (LogLevel.Error, $"Location change to '{new Uri(_serverFixture.RootUri,"/test")}' in circuit '{Client.CircuitId}' failed.");
}); });
} }
@ -373,6 +494,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
TestSink.MessageLogged -= LogMessages; TestSink.MessageLogged -= LogMessages;
} }
private string Serialize<T>(T browserEventDescriptor) =>
JsonSerializer.Serialize(browserEventDescriptor, TestJsonSerializerOptionsProvider.Options);
[DebuggerDisplay("{LogLevel.ToString(),nq} - {Message ?? \"null\",nq} - {Exception?.Message,nq}")]
private class LogMessage private class LogMessage
{ {
public LogMessage(LogLevel logLevel, string message, Exception exception) public LogMessage(LogLevel logLevel, string message, Exception exception)
@ -394,7 +519,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
private class Batch private class Batch
{ {
public Batch(int id, byte [] data) public Batch(int id, byte[] data)
{ {
Id = id; Id = id;
Data = data; Data = data;

View File

@ -9,8 +9,11 @@ using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ignitor; using Ignitor;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.SignalR.Client; using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.AspNetCore.Testing;
using Microsoft.AspNetCore.Testing.xunit;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Logging.Testing;
@ -19,6 +22,7 @@ using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{ {
[Flaky("https://github.com/aspnet/AspNetCore/issues/13086", FlakyOn.All)]
public class InteropReliabilityTests : IClassFixture<AspNetSiteServerFixture>, IDisposable public class InteropReliabilityTests : IClassFixture<AspNetSiteServerFixture>, IDisposable
{ {
private static readonly TimeSpan DefaultLatencyTimeout = TimeSpan.FromSeconds(30); private static readonly TimeSpan DefaultLatencyTimeout = TimeSpan.FromSeconds(30);
@ -260,6 +264,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
} }
[Fact] [Fact]
[Flaky("https://github.com/aspnet/AspNetCore/issues/13086", FlakyOn.AzP.Windows)]
public async Task ContinuesWorkingAfterInvalidAsyncReturnCallback() public async Task ContinuesWorkingAfterInvalidAsyncReturnCallback()
{ {
// Arrange // Arrange
@ -500,8 +505,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
// Arrange // Arrange
await GoToTestComponent(Batches); await GoToTestComponent(Batches);
var sink = _serverFixture.Host.Services.GetRequiredService<TestSink>(); var sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
var logEvents = new List<(LogLevel logLevel, string)>(); var logEvents = new List<(LogLevel logLevel, string eventIdName, Exception exception)>();
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name)); sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name, wc.Exception));
// Act // Act
var browserDescriptor = new WebEventDescriptor() var browserDescriptor = new WebEventDescriptor()
@ -520,8 +525,9 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
}); });
Assert.Contains( Assert.Contains(
(LogLevel.Debug, "DispatchEventFailedToParseEventData"), logEvents,
logEvents); e => e.eventIdName == "DispatchEventFailedToParseEventData" && e.logLevel == LogLevel.Debug &&
e.exception.Message == "There was an error parsing the event arguments. EventId: '6'.");
// Taking any other action will fail because the circuit is disposed. // Taking any other action will fail because the circuit is disposed.
await Client.ExpectCircuitErrorAndDisconnect(async () => await Client.ExpectCircuitErrorAndDisconnect(async () =>
@ -563,7 +569,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
Assert.Contains( Assert.Contains(
logEvents, logEvents,
e => e.eventIdName == "DispatchEventFailedToDispatchEvent" && e.logLevel == LogLevel.Debug && e => e.eventIdName == "DispatchEventFailedToDispatchEvent" && e.logLevel == LogLevel.Debug &&
e.exception is ArgumentException ae && ae.Message.Contains("There is no event handler with ID 1")); e.exception is ArgumentException ae && ae.Message.Contains("There is no event handler associated with this event. EventId: '1'."));
// Taking any other action will fail because the circuit is disposed. // Taking any other action will fail because the circuit is disposed.
await Client.ExpectCircuitErrorAndDisconnect(async () => await Client.ExpectCircuitErrorAndDisconnect(async () =>

View File

@ -33,7 +33,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
appElement.FindElement(By.Id("run-without-dispatch")).Click(); appElement.FindElement(By.Id("run-without-dispatch")).Click();
Browser.Contains( Browser.Contains(
$"{typeof(InvalidOperationException).FullName}: The current thread is not associated with the Dispatcher. Use Invoke() or InvokeAsync() to switch execution to the Dispatcher when triggering rendering or modifying any state accessed during rendering.", $"{typeof(InvalidOperationException).FullName}: The current thread is not associated with the Dispatcher. Use InvokeAsync() to switch execution to the Dispatcher when triggering rendering or component state.",
() => result.Text); () => result.Text);
} }
} }

View File

@ -1,6 +1,6 @@
<h1>Counter</h1> <h1>Counter</h1>
<p>Current count: @currentCount</p> <p>Current count: @currentCount</p>
<p><button @onclick="@(handleClicks ? (Action)IncrementCount : null)">Click me</button></p> <p><button id="counter" @onclick="@(handleClicks ? (Action)IncrementCount : null)">Click me</button></p>
<label> <label>
<input type="checkbox" @bind="handleClicks" /> <input type="checkbox" @bind="handleClicks" />

View File

@ -1,7 +1,6 @@
@page "/prerendered-interop" @page "/prerendered-interop"
@using Microsoft.AspNetCore.Components @using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop @using Microsoft.JSInterop
@inject IComponentContext ComponentContext
@inject IJSRuntime JSRuntime @inject IJSRuntime JSRuntime
<p> <p>

View File

@ -1,7 +1,6 @@
@page "/prerendered-transition" @page "/prerendered-transition"
@using Microsoft.AspNetCore.Components @using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Authorization
@inject IComponentContext ComponentContext
<CascadingAuthenticationState> <CascadingAuthenticationState>
<AuthorizeView> <AuthorizeView>
@ -11,7 +10,7 @@
<p> <p>
Current state: Current state:
<strong id="connected-state">@(ComponentContext.IsConnected ? "connected" : "not connected")</strong> <strong id="connected-state">@(hasRenderedInConnectedMode ? "connected" : "not connected")</strong>
</p> </p>
<p> <p>
@ -22,11 +21,13 @@
</CascadingAuthenticationState> </CascadingAuthenticationState>
@code { @code {
bool hasRenderedInConnectedMode;
int count; int count;
protected override Task OnAfterRenderAsync(bool firstRender) protected override Task OnAfterRenderAsync(bool firstRender)
{ {
if (firstRender) if (firstRender)
{ {
hasRenderedInConnectedMode = true;
// We need to queue another render when we connect, otherwise the // We need to queue another render when we connect, otherwise the
// browser won't see anything. // browser won't see anything.
StateHasChanged(); StateHasChanged();

View File

@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
namespace ComponentsApp.Server namespace ComponentsApp.Server
{ {
public class Startup public class Startup

View File

@ -3,10 +3,10 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Routing.Matching; using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -51,8 +51,6 @@ namespace Microsoft.AspNetCore.Routing
if (endpoint != null) if (endpoint != null)
{ {
Log.MatchSkipped(_logger, endpoint); Log.MatchSkipped(_logger, endpoint);
// Someone else set the endpoint, we'll let them handle the clearing of the endpoint.
return _next(httpContext); return _next(httpContext);
} }
@ -88,7 +86,8 @@ namespace Microsoft.AspNetCore.Routing
} }
private async Task SetRoutingAndContinue(HttpContext httpContext) [MethodImpl(MethodImplOptions.AggressiveInlining)]
private Task SetRoutingAndContinue(HttpContext httpContext)
{ {
// If there was no mutation of the endpoint then log failure // If there was no mutation of the endpoint then log failure
var endpoint = httpContext.GetEndpoint(); var endpoint = httpContext.GetEndpoint();
@ -108,18 +107,7 @@ namespace Microsoft.AspNetCore.Routing
Log.MatchSuccess(_logger, endpoint); Log.MatchSuccess(_logger, endpoint);
} }
try return _next(httpContext);
{
await _next(httpContext);
}
finally
{
// This allows a second call in a single request (such as from the ErrorHandlerMiddleware) to perform routing again.
httpContext.SetEndpoint(endpoint: null);
var routeValuesFeature = httpContext.Features.Get<IRouteValuesFeature>();
routeValuesFeature?.RouteValues?.Clear();
}
} }
// Initialization is async to avoid blocking threads while reflection and things // Initialization is async to avoid blocking threads while reflection and things

View File

@ -74,7 +74,7 @@ namespace Microsoft.AspNetCore.Builder
} }
[Fact] [Fact]
public async Task UseRouting_ServicesRegistered_Match_DoesNotSetFeature() public async Task UseRouting_ServicesRegistered_Match_DoesNotSetsFeature()
{ {
// Arrange // Arrange
var endpoint = new RouteEndpoint( var endpoint = new RouteEndpoint(
@ -104,6 +104,7 @@ namespace Microsoft.AspNetCore.Builder
// Assert // Assert
var feature = httpContext.Features.Get<IEndpointFeature>(); var feature = httpContext.Features.Get<IEndpointFeature>();
Assert.NotNull(feature); Assert.NotNull(feature);
Assert.Same(endpoint, httpContext.GetEndpoint());
} }
[Fact] [Fact]

View File

@ -21,72 +21,7 @@ namespace Microsoft.AspNetCore.Routing
public class EndpointRoutingMiddlewareTest public class EndpointRoutingMiddlewareTest
{ {
[Fact] [Fact]
public async Task Invoke_ChangedPath_ResultsInDifferentResult() public async Task Invoke_OnCall_SetsEndpointFeature()
{
// Arrange
var httpContext = CreateHttpContext();
var matcher = new Mock<Matcher>();
var pathToEndpoints = new Dictionary<string, Endpoint>()
{
["/initial"] = new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(), "initialEndpoint"),
["/changed"] = new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(), "changedEndpoint")
};
matcher.Setup(m => m.MatchAsync(httpContext))
.Callback<HttpContext>(context =>
{
var endpointToSet = pathToEndpoints[context.Request.Path];
context.SetEndpoint(endpointToSet);
})
.Returns(Task.CompletedTask)
.Verifiable();
var matcherFactory = Mock.Of<MatcherFactory>(factory => factory.CreateMatcher(It.IsAny<EndpointDataSource>()) == matcher.Object);
var middleware = CreateMiddleware(
matcherFactory: matcherFactory,
next: context =>
{
Assert.True(pathToEndpoints.TryGetValue(context.Request.Path, out var expectedEndpoint));
var currentEndpoint = context.GetEndpoint();
Assert.Equal(expectedEndpoint, currentEndpoint);
return Task.CompletedTask;
});
// Act
httpContext.Request.Path = "/initial";
await middleware.Invoke(httpContext);
httpContext.Request.Path = "/changed";
await middleware.Invoke(httpContext);
// Assert
matcher.Verify();
}
[Fact]
public async Task Invoke_OnException_ResetsEndpoint()
{
// Arrange
var httpContext = CreateHttpContext();
var middleware = CreateMiddleware(next: context => throw new Exception());
// Act
try
{
await middleware.Invoke(httpContext);
}
catch
{
// Do nothing, we expect the test to throw.
}
// Assert
var endpoint = httpContext.GetEndpoint();
Assert.Null(endpoint);
}
[Fact]
public async Task Invoke_OnCall_SetsEndpointFeatureAndResetsEndpoint()
{ {
// Arrange // Arrange
var httpContext = CreateHttpContext(); var httpContext = CreateHttpContext();
@ -99,36 +34,14 @@ namespace Microsoft.AspNetCore.Routing
// Assert // Assert
var endpointFeature = httpContext.Features.Get<IEndpointFeature>(); var endpointFeature = httpContext.Features.Get<IEndpointFeature>();
Assert.NotNull(endpointFeature); Assert.NotNull(endpointFeature);
Assert.Null(endpointFeature.Endpoint);
} }
[Fact] [Fact]
public async Task Invoke_OnCall_SetsEndpointFeatureAndResetsRouteValues() public async Task Invoke_SkipsRouting_IfEndpointSet()
{ {
// Arrange // Arrange
var httpContext = CreateHttpContext(); var httpContext = CreateHttpContext();
var initialRouteData = new RouteData(); httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(), "myapp"));
initialRouteData.Values["test"] = true;
httpContext.Features.Set<IRoutingFeature>(new RoutingFeature()
{
RouteData = initialRouteData,
});
var middleware = CreateMiddleware();
// Act
await middleware.Invoke(httpContext);
// Assert
Assert.Null(httpContext.GetRouteValue("test"));
}
[Fact]
public async Task Invoke_SkipsRoutingAndMaintainsEndpoint_IfEndpointSet()
{
// Arrange
var httpContext = CreateHttpContext();
var expectedEndpoint = new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(), "myapp");
httpContext.SetEndpoint(expectedEndpoint);
var middleware = CreateMiddleware(); var middleware = CreateMiddleware();
@ -137,7 +50,7 @@ namespace Microsoft.AspNetCore.Routing
// Assert // Assert
var endpoint = httpContext.GetEndpoint(); var endpoint = httpContext.GetEndpoint();
Assert.Same(expectedEndpoint, endpoint); Assert.NotNull(endpoint);
Assert.Equal("myapp", endpoint.DisplayName); Assert.Equal("myapp", endpoint.DisplayName);
} }
@ -182,29 +95,22 @@ namespace Microsoft.AspNetCore.Routing
{ {
// Arrange // Arrange
var httpContext = CreateHttpContext(); var httpContext = CreateHttpContext();
var nextCalled = false;
var middleware = CreateMiddleware(next: context => var middleware = CreateMiddleware();
{
var routeData = httpContext.GetRouteData();
var routeValue = httpContext.GetRouteValue("controller");
var routeValuesFeature = httpContext.Features.Get<IRouteValuesFeature>();
nextCalled = true;
// Assert // Act
Assert.NotNull(routeData);
Assert.Equal("Home", (string)routeValue);
// changing route data value is reflected in endpoint feature values
routeData.Values["testKey"] = "testValue";
Assert.Equal("testValue", routeValuesFeature.RouteValues["testKey"]);
return Task.CompletedTask;
});
// Act & Assert
await middleware.Invoke(httpContext); await middleware.Invoke(httpContext);
Assert.True(nextCalled); var routeData = httpContext.GetRouteData();
var routeValue = httpContext.GetRouteValue("controller");
var routeValuesFeature = httpContext.Features.Get<IRouteValuesFeature>();
// Assert
Assert.NotNull(routeData);
Assert.Equal("Home", (string)routeValue);
// changing route data value is reflected in endpoint feature values
routeData.Values["testKey"] = "testValue";
Assert.Equal("testValue", routeValuesFeature.RouteValues["testKey"]);
} }
[Fact] [Fact]
@ -212,29 +118,22 @@ namespace Microsoft.AspNetCore.Routing
{ {
// Arrange // Arrange
var httpContext = CreateHttpContext(); var httpContext = CreateHttpContext();
var called = false;
var middleware = CreateMiddleware(next: context => var middleware = CreateMiddleware();
{
var routeData = httpContext.GetRouteData();
var routeValue = httpContext.GetRouteValue("controller");
var routeValuesFeature = httpContext.Features.Get<IRouteValuesFeature>();
called = true;
// Assert // Act
Assert.NotNull(routeData);
Assert.Equal("Home", (string)routeValue);
// changing route data value is reflected in endpoint feature values
routeData.Values["testKey"] = "testValue";
Assert.Equal("testValue", routeValuesFeature.RouteValues["testKey"]);
return Task.CompletedTask;
});
// Act & Assert
await middleware.Invoke(httpContext); await middleware.Invoke(httpContext);
Assert.True(called); var routeData = httpContext.GetRouteData();
var routeValue = httpContext.GetRouteValue("controller");
var routeValuesFeature = httpContext.Features.Get<IRouteValuesFeature>();
// Assert
Assert.NotNull(routeData);
Assert.Equal("Home", (string)routeValue);
// changing route data value is reflected in endpoint feature values
routeData.Values["testKey"] = "testValue";
Assert.Equal("testValue", routeValuesFeature.RouteValues["testKey"]);
} }
[Fact] [Fact]

View File

@ -7,6 +7,7 @@ using System.Runtime.ExceptionServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
@ -103,7 +104,8 @@ namespace Microsoft.AspNetCore.Diagnostics
} }
try try
{ {
context.Response.Clear(); ClearHttpContext(context);
var exceptionHandlerFeature = new ExceptionHandlerFeature() var exceptionHandlerFeature = new ExceptionHandlerFeature()
{ {
Error = edi.SourceException, Error = edi.SourceException,
@ -137,6 +139,17 @@ namespace Microsoft.AspNetCore.Diagnostics
edi.Throw(); // Re-throw the original if we couldn't handle it edi.Throw(); // Re-throw the original if we couldn't handle it
} }
private static void ClearHttpContext(HttpContext context)
{
context.Response.Clear();
// An endpoint may have already been set. Since we're going to re-invoke the middleware pipeline we need to reset
// the endpoint and route values to ensure things are re-calculated.
context.SetEndpoint(endpoint: null);
var routeValuesFeature = context.Features.Get<IRouteValuesFeature>();
routeValuesFeature?.RouteValues?.Clear();
}
private static Task ClearCacheHeaders(object state) private static Task ClearCacheHeaders(object state)
{ {
var headers = ((HttpResponse)state).Headers; var headers = ((HttpResponse)state).Headers;

View File

@ -0,0 +1,87 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Diagnostics
{
public class ExceptionHandlerMiddlewareTest
{
[Fact]
public async Task Invoke_ExceptionThrownResultsInClearedRouteValuesAndEndpoint()
{
// Arrange
var httpContext = CreateHttpContext();
httpContext.SetEndpoint(new Endpoint((_) => Task.CompletedTask, new EndpointMetadataCollection(), "Test"));
httpContext.Request.RouteValues["John"] = "Doe";
var optionsAccessor = CreateOptionsAccessor(
exceptionHandler: context =>
{
Assert.Empty(context.Request.RouteValues);
Assert.Null(context.GetEndpoint());
return Task.CompletedTask;
});
var middleware = CreateMiddleware(_ => throw new InvalidOperationException(), optionsAccessor);
// Act & Assert
await middleware.Invoke(httpContext);
}
private HttpContext CreateHttpContext()
{
var httpContext = new DefaultHttpContext
{
RequestServices = new TestServiceProvider()
};
return httpContext;
}
private IOptions<ExceptionHandlerOptions> CreateOptionsAccessor(
RequestDelegate exceptionHandler = null,
string exceptionHandlingPath = null)
{
exceptionHandler ??= c => Task.CompletedTask;
var options = new ExceptionHandlerOptions()
{
ExceptionHandler = exceptionHandler,
ExceptionHandlingPath = exceptionHandlingPath,
};
var optionsAccessor = Mock.Of<IOptions<ExceptionHandlerOptions>>(o => o.Value == options);
return optionsAccessor;
}
private ExceptionHandlerMiddleware CreateMiddleware(
RequestDelegate next,
IOptions<ExceptionHandlerOptions> options)
{
next ??= c => Task.CompletedTask;
var listener = new DiagnosticListener("Microsoft.AspNetCore");
var middleware = new ExceptionHandlerMiddleware(
next,
NullLoggerFactory.Instance,
options,
listener);
return middleware;
}
private class TestServiceProvider : IServiceProvider
{
public object GetService(Type serviceType)
{
throw new NotImplementedException();
}
}
}
}

View File

@ -208,7 +208,6 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddScoped<StaticComponentRenderer>(); services.TryAddScoped<StaticComponentRenderer>();
services.TryAddScoped<NavigationManager, HttpNavigationManager>(); services.TryAddScoped<NavigationManager, HttpNavigationManager>();
services.TryAddScoped<IJSRuntime, UnsupportedJavaScriptRuntime>(); services.TryAddScoped<IJSRuntime, UnsupportedJavaScriptRuntime>();
services.TryAddScoped<IComponentContext, UnsupportedComponentContext>();
services.TryAddScoped<INavigationInterception, UnsupportedNavigationInterception>(); services.TryAddScoped<INavigationInterception, UnsupportedNavigationInterception>();
services.TryAddTransient<ControllerSaveTempDataPropertyFilter>(); services.TryAddTransient<ControllerSaveTempDataPropertyFilter>();

View File

@ -1,12 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Components;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
internal class UnsupportedComponentContext : IComponentContext
{
public bool IsConnected => false;
}
}

View File

@ -6,6 +6,7 @@
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
<IsTestProject>false</IsTestProject> <IsTestProject>false</IsTestProject>
<IsShippingPackage>true</IsShippingPackage> <IsShippingPackage>true</IsShippingPackage>
<PackOnBuild>true</PackOnBuild>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -9,58 +9,60 @@ namespace SignalRSamples.Hubs
{ {
public class Chat : Hub public class Chat : Hub
{ {
public override async Task OnConnectedAsync() public override Task OnConnectedAsync()
{ {
await Clients.All.SendAsync("Send", $"{Context.ConnectionId} joined"); var name = Context.GetHttpContext().Request.Query["name"];
return Clients.All.SendAsync("Send", $"{name} joined the chat");
} }
public override async Task OnDisconnectedAsync(Exception ex) public override Task OnDisconnectedAsync(Exception exception)
{ {
await Clients.Others.SendAsync("Send", $"{Context.ConnectionId} left"); var name = Context.GetHttpContext().Request.Query["name"];
return Clients.All.SendAsync("Send", $"{name} left the chat");
} }
public Task Send(string message) public Task Send(string name, string message)
{ {
return Clients.All.SendAsync("Send", $"{Context.ConnectionId}: {message}"); return Clients.All.SendAsync("Send", $"{name}: {message}");
} }
public Task SendToOthers(string message) public Task SendToOthers(string name, string message)
{ {
return Clients.Others.SendAsync("Send", $"{Context.ConnectionId}: {message}"); return Clients.Others.SendAsync("Send", $"{name}: {message}");
} }
public Task SendToConnection(string connectionId, string message) public Task SendToConnection(string connectionId, string name, string message)
{ {
return Clients.Client(connectionId).SendAsync("Send", $"Private message from {Context.ConnectionId}: {message}"); return Clients.Client(connectionId).SendAsync("Send", $"Private message from {name}: {message}");
} }
public Task SendToGroup(string groupName, string message) public Task SendToGroup(string groupName, string name ,string message)
{ {
return Clients.Group(groupName).SendAsync("Send", $"{Context.ConnectionId}@{groupName}: {message}"); return Clients.Group(groupName).SendAsync("Send", $"{name}@{groupName}: {message}");
} }
public Task SendToOthersInGroup(string groupName, string message) public Task SendToOthersInGroup(string groupName, string name,string message)
{ {
return Clients.OthersInGroup(groupName).SendAsync("Send", $"{Context.ConnectionId}@{groupName}: {message}"); return Clients.OthersInGroup(groupName).SendAsync("Send", $"{name}@{groupName}: {message}");
} }
public async Task JoinGroup(string groupName) public async Task JoinGroup(string groupName, string name)
{ {
await Groups.AddToGroupAsync(Context.ConnectionId, groupName); await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
await Clients.Group(groupName).SendAsync("Send", $"{Context.ConnectionId} joined {groupName}"); await Clients.Group(groupName).SendAsync("Send", $"{name} joined {groupName}");
} }
public async Task LeaveGroup(string groupName) public async Task LeaveGroup(string groupName, string name)
{ {
await Clients.Group(groupName).SendAsync("Send", $"{Context.ConnectionId} left {groupName}"); await Clients.Group(groupName).SendAsync("Send", $"{name} left {groupName}");
await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName); await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
} }
public Task Echo(string message) public Task Echo(string name, string message)
{ {
return Clients.Caller.SendAsync("Send", $"{Context.ConnectionId}: {message}"); return Clients.Caller.SendAsync("Send", $"{name}: {message}");
} }
} }
} }

View File

@ -9,53 +9,55 @@ namespace SignalRSamples.Hubs
{ {
public class DynamicChat : DynamicHub public class DynamicChat : DynamicHub
{ {
public override async Task OnConnectedAsync() public override Task OnConnectedAsync()
{ {
await Clients.All.Send($"{Context.ConnectionId} joined"); var name = Context.GetHttpContext().Request.Query["name"];
return Clients.All.Send($"{name} joined the chat");
} }
public override async Task OnDisconnectedAsync(Exception ex) public override Task OnDisconnectedAsync(Exception exception)
{ {
await Clients.Others.Send($"{Context.ConnectionId} left"); var name = Context.GetHttpContext().Request.Query["name"];
return Clients.All.Send($"{name} left the chat");
} }
public Task Send(string message) public Task Send(string name, string message)
{ {
return Clients.All.Send($"{Context.ConnectionId}: {message}"); return Clients.All.Send($"{name}: {message}");
} }
public Task SendToOthers(string message) public Task SendToOthers(string name, string message)
{ {
return Clients.Others.Send($"{Context.ConnectionId}: {message}"); return Clients.Others.Send($"{name}: {message}");
} }
public Task SendToGroup(string groupName, string message) public Task SendToGroup(string groupName, string name, string message)
{ {
return Clients.Group(groupName).Send($"{Context.ConnectionId}@{groupName}: {message}"); return Clients.Group(groupName).Send($"{name}@{groupName}: {message}");
} }
public Task SendToOthersInGroup(string groupName, string message) public Task SendToOthersInGroup(string groupName, string name, string message)
{ {
return Clients.OthersInGroup(groupName).Send($"{Context.ConnectionId}@{groupName}: {message}"); return Clients.OthersInGroup(groupName).Send($"{name}@{groupName}: {message}");
} }
public async Task JoinGroup(string groupName) public async Task JoinGroup(string groupName, string name)
{ {
await Groups.AddToGroupAsync(Context.ConnectionId, groupName); await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
await Clients.Group(groupName).Send($"{Context.ConnectionId} joined {groupName}"); await Clients.Group(groupName).Send($"{name} joined {groupName}");
} }
public async Task LeaveGroup(string groupName) public async Task LeaveGroup(string groupName, string name)
{ {
await Clients.Group(groupName).Send($"{Context.ConnectionId} left {groupName}"); await Clients.Group(groupName).Send($"{name} left {groupName}");
await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName); await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
} }
public Task Echo(string message) public Task Echo(string name, string message)
{ {
return Clients.Caller.Send($"{Context.ConnectionId}: {message}"); return Clients.Caller.Send($"{name}: {message}");
} }
} }
} }

View File

@ -9,53 +9,55 @@ namespace SignalRSamples.Hubs
{ {
public class HubTChat : Hub<IChatClient> public class HubTChat : Hub<IChatClient>
{ {
public override async Task OnConnectedAsync() public override Task OnConnectedAsync()
{ {
await Clients.All.Send($"{Context.ConnectionId} joined"); var name = Context.GetHttpContext().Request.Query["name"];
return Clients.All.Send($"{name} joined the chat");
} }
public override async Task OnDisconnectedAsync(Exception ex) public override Task OnDisconnectedAsync(Exception exception)
{ {
await Clients.Others.Send($"{Context.ConnectionId} left"); var name = Context.GetHttpContext().Request.Query["name"];
return Clients.All.Send($"{name} left the chat");
} }
public Task Send(string message) public Task Send(string name, string message)
{ {
return Clients.All.Send($"{Context.ConnectionId}: {message}"); return Clients.All.Send($"{name}: {message}");
} }
public Task SendToOthers(string message) public Task SendToOthers(string name, string message)
{ {
return Clients.Others.Send($"{Context.ConnectionId}: {message}"); return Clients.Others.Send($"{name}: {message}");
} }
public Task SendToGroup(string groupName, string message) public Task SendToGroup(string groupName, string name, string message)
{ {
return Clients.Group(groupName).Send($"{Context.ConnectionId}@{groupName}: {message}"); return Clients.Group(groupName).Send($"{name}@{groupName}: {message}");
} }
public Task SendToOthersInGroup(string groupName, string message) public Task SendToOthersInGroup(string groupName, string name, string message)
{ {
return Clients.OthersInGroup(groupName).Send($"{Context.ConnectionId}@{groupName}: {message}"); return Clients.OthersInGroup(groupName).Send($"{name}@{groupName}: {message}");
} }
public async Task JoinGroup(string groupName) public async Task JoinGroup(string groupName, string name)
{ {
await Groups.AddToGroupAsync(Context.ConnectionId, groupName); await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
await Clients.Group(groupName).Send($"{Context.ConnectionId} joined {groupName}"); await Clients.Group(groupName).Send($"{name} joined {groupName}");
} }
public async Task LeaveGroup(string groupName) public async Task LeaveGroup(string groupName, string name)
{ {
await Clients.Group(groupName).Send($"{Context.ConnectionId} left {groupName}"); await Clients.Group(groupName).Send($"{name} left {groupName}");
await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName); await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
} }
public Task Echo(string message) public Task Echo(string name, string message)
{ {
return Clients.Caller.Send($"{Context.ConnectionId}: {message}"); return Clients.Caller.Send($"{name}: {message}");
} }
} }

View File

@ -27,6 +27,7 @@
<option value="hubT">Hub&lt;T&gt;</option> <option value="hubT">Hub&lt;T&gt;</option>
</select> </select>
<input type="text" id="displayname" placeholder="Enter User Name" />
<input type="button" id="connect" value="Connect" /> <input type="button" id="connect" value="Connect" />
<input type="button" id="disconnect" value="Disconnect" /> <input type="button" id="disconnect" value="Disconnect" />
@ -58,6 +59,7 @@
<div class="input-append"> <div class="input-append">
<input type="text" id="me-message-text" placeholder="Type a message" /> <input type="text" id="me-message-text" placeholder="Type a message" />
<input type="button" id="send" class="btn" value="Send to Me" /> <input type="button" id="send" class="btn" value="Send to Me" />
<input type="hidden" id="displayname" />
</div> </div>
</form> </form>
@ -138,6 +140,13 @@
var connection; var connection;
click('connect', function (event) { click('connect', function (event) {
name = document.getElementById("displayname").value
if (name === "") {
alert("Please enter a valid name");
return;
}
let hubRoute = hubTypeDropdown.value || "default"; let hubRoute = hubTypeDropdown.value || "default";
let protocol = protocolDropdown.value === "msgpack" ? let protocol = protocolDropdown.value === "msgpack" ?
new signalR.protocols.msgpack.MessagePackHubProtocol() : new signalR.protocols.msgpack.MessagePackHubProtocol() :
@ -148,6 +157,7 @@
options.transport = signalR.HttpTransportType[transportDropdown.value]; options.transport = signalR.HttpTransportType[transportDropdown.value];
} }
hubRoute = hubRoute + "?name=" + name;
console.log('http://' + document.location.host + '/' + hubRoute); console.log('http://' + document.location.host + '/' + hubRoute);
var connectionBuilder = new signalR.HubConnectionBuilder() var connectionBuilder = new signalR.HubConnectionBuilder()
@ -205,45 +215,45 @@
click('broadcast', function (event) { click('broadcast', function (event) {
let data = getText('message-text'); let data = getText('message-text');
invoke(connection, 'Send', data); invoke(connection, 'Send', name, data);
}); });
click('join-group', function (event) { click('join-group', function (event) {
let groupName = getText('group-name'); let groupName = getText('group-name');
invoke(connection, 'JoinGroup', groupName); invoke(connection, 'JoinGroup', groupName, name);
}); });
click('leave-group', function (event) { click('leave-group', function (event) {
let groupName = getText('group-name'); let groupName = getText('group-name');
invoke(connection, 'LeaveGroup', groupName); invoke(connection, 'LeaveGroup', groupName, name);
}); });
click('groupmsg', function (event) { click('groupmsg', function (event) {
let groupName = getText('group-name'); let groupName = getText('group-name');
let message = getText('group-message-text'); let message = getText('group-message-text');
invoke(connection, 'SendToGroup', groupName, message); invoke(connection, 'SendToGroup', groupName, name, message);
}); });
click('others-groupmsg', function (event) { click('others-groupmsg', function (event) {
let groupName = getText('group-name'); let groupName = getText('group-name');
let message = getText('group-message-text'); let message = getText('group-message-text');
invoke(connection, 'SendToOthersInGroup', groupName, message); invoke(connection, 'SendToOthersInGroup', groupName, name, message);
}); });
click('send', function (event) { click('send', function (event) {
let data = getText('me-message-text'); let data = getText('me-message-text');
invoke(connection, 'Echo', data); invoke(connection, 'Echo', name, data);
}); });
click('broadcast-exceptme', function (event) { click('broadcast-exceptme', function (event) {
let data = getText('message-text'); let data = getText('message-text');
invoke(connection, 'SendToOthers', data); invoke(connection, 'SendToOthers', name, data);
}); });
click('connection-send', function (event) { click('connection-send', function (event) {
let data = getText('connection-message-text'); let data = getText('connection-message-text');
let id = getText('connection-id'); let id = getText('connection-id');
invoke(connection, 'SendToConnection', id, data); invoke(connection, 'SendToConnection', id, name, data);
}); });
</script> </script>