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
```
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?

View File

@ -171,8 +171,13 @@ try {
# 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'
# Temporary: Disable check for blazor js file
$changedFilesExclusion = "src/Components/Web.JS/dist/Release/blazor.server.js"
if ($changedFiles) {
foreach ($file in $changedFiles) {
if ($file -eq $changedFilesExclusion) {continue}
$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
& git --no-pager diff --ignore-space-at-eol $filePath

View File

@ -11,7 +11,7 @@
<HelixAvailablePlatform Include="OSX" />
<HelixAvailablePlatform Include="Linux" />
</ItemGroup>
<!-- x64 queues -->
<ItemGroup Condition="'$(IsWindowsOnlyTest)' != 'true' AND '$(TargetArchitecture)' == 'x64'">
<HelixAvailableTargetQueue Include="Windows.10.Amd64.ClientRS4.VS2017.Open" Platform="Windows" />
@ -27,11 +27,11 @@
<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" />
</ItemGroup>
<ItemGroup Condition="'$(IsWindowsOnlyTest)' != 'true' AND '$(TargetArchitecture)' == 'arm64'">
<!-- 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
<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" />

View File

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

View File

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

View File

@ -91,7 +91,6 @@ namespace Microsoft.AspNetCore.Blazor.Hosting
services.AddSingleton(_BrowserHostBuilderContext);
services.AddSingleton<IWebAssemblyHost, WebAssemblyHost>();
services.AddSingleton<IJSRuntime>(WebAssemblyJSRuntime.Instance);
services.AddSingleton<IComponentContext, WebAssemblyComponentContext>();
services.AddSingleton<NavigationManager>(WebAssemblyNavigationManager.Instance);
services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance);
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
{
protected Dispatcher() { }
public void AssertAccess() { }
public abstract bool CheckAccess();
public static Microsoft.AspNetCore.Components.Dispatcher CreateDefault() { throw null; }
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);
System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters);
}
public partial interface IComponentContext
{
bool IsConnected { get; }
}
public partial interface IHandleAfterRender
{
System.Threading.Tasks.Task OnAfterRenderAsync();

View File

@ -97,6 +97,7 @@ namespace Microsoft.AspNetCore.Components
public abstract partial class Dispatcher
{
protected Dispatcher() { }
public void AssertAccess() { }
public abstract bool CheckAccess();
public static Microsoft.AspNetCore.Components.Dispatcher CreateDefault() { throw null; }
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);
System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters);
}
public partial interface IComponentContext
{
bool IsConnected { get; }
}
public partial interface IHandleAfterRender
{
System.Threading.Tasks.Task OnAfterRenderAsync();

View File

@ -23,6 +23,20 @@ namespace Microsoft.AspNetCore.Components
/// </summary>
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>
/// Returns a value that determines whether using the dispatcher to invoke a work item is required
/// 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>
public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo fieldInfo, EventArgs eventArgs)
{
EnsureSynchronizationContext();
Dispatcher.AssertAccess();
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);
@ -337,7 +337,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// <param name="renderFragment">A <see cref="RenderFragment"/> that will supply the updated UI contents.</param>
internal void AddToRenderQueue(int componentId, RenderFragment renderFragment)
{
EnsureSynchronizationContext();
Dispatcher.AssertAccess();
var componentState = GetOptionalComponentState(componentId);
if (componentState == null)
@ -374,21 +374,6 @@ namespace Microsoft.AspNetCore.Components.Rendering
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)
=> _componentStateById.TryGetValue(componentId, out var componentState)
? componentState
@ -414,7 +399,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
private void ProcessRenderQueue()
{
EnsureSynchronizationContext();
Dispatcher.AssertAccess();
if (_isBatchInProgress)
{

View File

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

View File

@ -2883,7 +2883,7 @@ namespace Microsoft.AspNetCore.Components.Test
{
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);
}

View File

@ -18,7 +18,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{
internal class CircuitHost : IAsyncDisposable
{
private readonly SemaphoreSlim HandlerLock = new SemaphoreSlim(1);
private readonly IServiceScope _scope;
private readonly CircuitOptions _options;
private readonly CircuitHandler[] _circuitHandlers;
@ -27,24 +26,9 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
private bool _disposed;
/// <summary>
/// Sets the current <see cref="Circuits.Circuit"/>.
/// Sets the <see cref="IJSRuntime"/> for the current excution context.
/// </summary>
/// <param name="circuitHost">The <see cref="Circuits.Circuit"/>.</param>
/// <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);
}
public void SetCurrentJSRuntime() => JSInterop.JSRuntime.SetCurrentJSRuntime(JSRuntime);
// This event is fired when there's an unrecoverable exception coming from the circuit, and
// it need so be torn down. The registry listens to this even so that the circuit can
@ -116,7 +100,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
try
{
SetCurrentCircuitHost(this);
SetCurrentJSRuntime();
_initialized = true; // We're ready to accept incoming JSInterop calls from here on
await OnCircuitOpenedAsync(cancellationToken);
@ -186,7 +170,18 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
try
{
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);
}
catch (Exception ex)
@ -202,35 +197,28 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{
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;
for (var i = 0; i < _circuitHandlers.Length; i++)
var circuitHandler = _circuitHandlers[i];
try
{
var circuitHandler = _circuitHandlers[i];
try
{
await circuitHandler.OnCircuitOpenedAsync(Circuit, cancellationToken);
}
catch (Exception ex)
{
Log.CircuitHandlerFailed(_logger, circuitHandler, nameof(CircuitHandler.OnCircuitOpenedAsync), ex);
exceptions ??= new List<Exception>();
exceptions.Add(ex);
}
await circuitHandler.OnCircuitOpenedAsync(Circuit, cancellationToken);
}
if (exceptions != null)
catch (Exception ex)
{
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);
await HandlerLock.WaitAsync(cancellationToken);
Renderer.Dispatcher.AssertAccess();
List<Exception> exceptions = null;
try
for (var i = 0; i < _circuitHandlers.Length; i++)
{
List<Exception> exceptions = null;
for (var i = 0; i < _circuitHandlers.Length; i++)
var circuitHandler = _circuitHandlers[i];
try
{
var circuitHandler = _circuitHandlers[i];
try
{
await circuitHandler.OnConnectionUpAsync(Circuit, cancellationToken);
}
catch (Exception ex)
{
Log.CircuitHandlerFailed(_logger, circuitHandler, nameof(CircuitHandler.OnConnectionUpAsync), ex);
exceptions ??= new List<Exception>();
exceptions.Add(ex);
}
await circuitHandler.OnConnectionUpAsync(Circuit, cancellationToken);
}
if (exceptions != null)
catch (Exception ex)
{
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);
await HandlerLock.WaitAsync(cancellationToken);
Renderer.Dispatcher.AssertAccess();
List<Exception> exceptions = null;
try
for (var i = 0; i < _circuitHandlers.Length; i++)
{
List<Exception> exceptions = null;
for (var i = 0; i < _circuitHandlers.Length; i++)
var circuitHandler = _circuitHandlers[i];
try
{
var circuitHandler = _circuitHandlers[i];
try
{
await circuitHandler.OnConnectionDownAsync(Circuit, cancellationToken);
}
catch (Exception ex)
{
Log.CircuitHandlerFailed(_logger, circuitHandler, nameof(CircuitHandler.OnConnectionDownAsync), ex);
exceptions ??= new List<Exception>();
exceptions.Add(ex);
}
await circuitHandler.OnConnectionDownAsync(Circuit, cancellationToken);
}
if (exceptions != null)
catch (Exception ex)
{
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);
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
{
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);
}
_ = Renderer.OnRenderCompletedAsync(renderId, errorMessageOrNull);
}
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(() =>
{
SetCurrentCircuitHost(this);
SetCurrentJSRuntime();
Log.BeginInvokeDotNet(_logger, callId, assemblyName, methodIdentifier, dotNetObjectId);
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
// however, we still want this to get logged if we do.
Log.BeginInvokeDotNetFailed(_logger, callId, assemblyName, methodIdentifier, dotNetObjectId, ex);
if (Client.Connected)
{
await NotifyClientError(Client, "Interop call failed.");
}
await TryNotifyClientErrorAsync(Client, GetClientErrorMessage(ex, "Interop call failed."));
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false));
}
}
@ -382,7 +366,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{
await Renderer.Dispatcher.InvokeAsync(() =>
{
SetCurrentCircuitHost(this);
SetCurrentJSRuntime();
if (!succeded)
{
// 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
// client won't do this.
Log.EndInvokeDispatchException(_logger, ex);
if (Client.Connected)
{
await NotifyClientError(Client, "Invalid interop arguments.");
}
await TryNotifyClientErrorAsync(Client, GetClientErrorMessage(ex, "Invalid interop arguments."));
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false));
}
}
@ -419,17 +400,13 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
WebEventData webEventData;
try
{
AssertInitialized();
webEventData = WebEventData.Parse(eventDescriptorJson, eventArgsJson);
}
catch (Exception ex)
{
// Invalid event data is fatal. We expect a well-behaved client to send valid JSON.
Log.DispatchEventFailedToParseEventData(_logger, ex);
if (Client.Connected)
{
await NotifyClientError(Client, "Invalid event data.");
}
await TryNotifyClientErrorAsync(Client, GetClientErrorMessage(ex, "Bad input data."));
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false));
return;
}
@ -438,7 +415,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{
await Renderer.Dispatcher.InvokeAsync(() =>
{
SetCurrentCircuitHost(this);
SetCurrentJSRuntime();
return Renderer.DispatchEventAsync(
webEventData.EventHandlerId,
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 well-behaved client won't do this.
Log.DispatchEventFailedToDispatchEvent(_logger, webEventData.EventHandlerId.ToString(), ex);
if (Client.Connected)
{
await NotifyClientError(Client, "Failed to dispatch event.");
}
await TryNotifyClientErrorAsync(Client, GetClientErrorMessage(ex, "Failed to dispatch event."));
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false));
}
}
@ -469,7 +443,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{
await Renderer.Dispatcher.InvokeAsync(() =>
{
SetCurrentCircuitHost(this);
SetCurrentJSRuntime();
Log.LocationChange(_logger, uri, CircuitId);
var navigationManager = (RemoteNavigationManager)Services.GetRequiredService<NavigationManager>();
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
// exception in user-code.
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));
}
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
// this like bad data.
Log.LocationChangeFailed(_logger, uri, CircuitId, ex);
if (Client.Connected)
{
await NotifyClientError(Client, $"Location change to {uri} failed.");
}
await TryNotifyClientErrorAsync(Client, GetClientErrorMessage(ex, $"Location change to '{uri}' failed."));
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.
// 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());
}
@ -566,39 +537,50 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
private async Task ReportUnhandledException(Exception 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)
{
_logger.LogDebug("Client is disconnected YO.");
Log.UnhandledExceptionClientDisconnected(
_logger,
CircuitId,
exception);
return;
}
try
{
if (_options.DetailedErrors)
{
await NotifyClientError(Client, exception.ToString());
}
else
{
var message =
$"There was an unhandled exception on the current circuit, so this circuit will be terminated. For more details turn on " +
$"detailed exceptions in '{typeof(CircuitOptions).Name}.{nameof(CircuitOptions.DetailedErrors)}'";
await NotifyClientError(Client, message);
}
Log.CircuitTransmittingClientError(_logger, CircuitId);
await client.SendAsync("JS.Error", error);
Log.CircuitTransmittedClientErrorSuccess(_logger, CircuitId);
}
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 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, Type, string, string, Exception> _circuitHandlerFailed;
private static readonly Action<ILogger, string, Exception> _circuitUnhandledException;
private static readonly Action<ILogger, string, Exception> _circuitUnhandledExceptionFailed;
private static readonly Action<ILogger, string, 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, 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> _locationChangeFailed;
private static readonly Action<ILogger, string, string, Exception> _locationChangeFailedInCircuit;
private static readonly Action<ILogger, long, string, Exception> _onRenderCompletedFailed;
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 CircuitHandlerFailed = new EventId(110, "CircuitHandlerFailed");
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
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 LocationChangeFailed = new EventId(210, "LocationChangeFailed");
public static readonly EventId LocationChangeFailedInCircuit = new EventId(211, "LocationChangeFailedInCircuit");
public static readonly EventId OnRenderCompletedFailed = new EventId(212, " OnRenderCompletedFailed");
}
static Log()
@ -666,87 +656,102 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
_intializationStarted = LoggerMessage.Define(
LogLevel.Debug,
EventIds.InitializationFailed,
"Circuit initialization started");
"Circuit initialization started.");
_intializationSucceded = LoggerMessage.Define(
LogLevel.Debug,
EventIds.InitializationFailed,
"Circuit initialization succeeded");
"Circuit initialization succeeded.");
_intializationFailed = LoggerMessage.Define(
LogLevel.Debug,
EventIds.InitializationFailed,
"Circuit initialization failed");
"Circuit initialization failed.");
_disposeStarted = LoggerMessage.Define<string>(
LogLevel.Debug,
EventIds.DisposeStarted,
"Disposing circuit {CircuitId} started");
"Disposing circuit '{CircuitId}' started.");
_disposeSucceded = LoggerMessage.Define<string>(
LogLevel.Debug,
EventIds.DisposeSucceeded,
"Disposing circuit {CircuitId} succeded");
"Disposing circuit '{CircuitId}' succeded.");
_disposeFailed = LoggerMessage.Define<string>(
LogLevel.Debug,
EventIds.DisposeFailed,
"Disposing circuit {CircuitId} failed");
"Disposing circuit '{CircuitId}' failed.");
_onCircuitOpened = LoggerMessage.Define<string>(
LogLevel.Debug,
EventIds.OnCircuitOpened,
"Opening circuit with id {CircuitId}.");
"Opening circuit with id '{CircuitId}'.");
_onConnectionUp = LoggerMessage.Define<string, string>(
LogLevel.Debug,
EventIds.OnConnectionUp,
"Circuit id {CircuitId} connected using connection {ConnectionId}.");
"Circuit id '{CircuitId}' connected using connection '{ConnectionId}'.");
_onConnectionDown = LoggerMessage.Define<string, string>(
LogLevel.Debug,
EventIds.OnConnectionDown,
"Circuit id {CircuitId} disconnected from connection {ConnectionId}.");
"Circuit id '{CircuitId}' disconnected from connection '{ConnectionId}'.");
_onCircuitClosed = LoggerMessage.Define<string>(
LogLevel.Debug,
EventIds.OnCircuitClosed,
"Closing circuit with id {CircuitId}.");
"Closing circuit with id '{CircuitId}'.");
_circuitHandlerFailed = LoggerMessage.Define<Type, string, string>(
LogLevel.Error,
EventIds.CircuitHandlerFailed,
"Unhandled error invoking circuit handler type {handlerType}.{handlerMethod}: {Message}");
_circuitUnhandledException = LoggerMessage.Define<string>(
LogLevel.Error,
EventIds.CircuitUnhandledException,
"Unhandled exception in circuit {CircuitId}");
_circuitUnhandledException = LoggerMessage.Define<string>(
LogLevel.Error,
EventIds.CircuitUnhandledException,
"Unhandled exception in circuit '{CircuitId}'.");
_circuitUnhandledExceptionFailed = LoggerMessage.Define<string>(
LogLevel.Debug,
EventIds.CircuitUnhandledExceptionFailed,
"Failed to transmit exception to client in circuit {CircuitId}");
_circuitTransmittingClientError = LoggerMessage.Define<string>(
LogLevel.Debug,
EventIds.CircuitTransmittingClientError,
"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>(
LogLevel.Debug,
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>(
LogLevel.Debug,
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>(
LogLevel.Debug,
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>(
LogLevel.Debug,
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(
LogLevel.Debug,
@ -776,25 +781,30 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
_locationChange = LoggerMessage.Define<string, string>(
LogLevel.Debug,
EventIds.LocationChange,
"Location changing to {URI} in circuit {CircuitId}.");
"Location changing to {URI} in circuit '{CircuitId}'.");
_locationChangeSucceeded = LoggerMessage.Define<string, string>(
LogLevel.Debug,
EventIds.LocationChangeSucceded,
"Location change to {URI} in circuit {CircuitId} succeded.");
"Location change to '{URI}' in circuit '{CircuitId}' succeded.");
_locationChangeFailed = LoggerMessage.Define<string, string>(
LogLevel.Debug,
EventIds.LocationChangeFailed,
"Location change to {URI} in circuit {CircuitId} failed.");
"Location change to '{URI}' in circuit '{CircuitId}' failed.");
_locationChangeFailedInCircuit = LoggerMessage.Define<string, string>(
LogLevel.Error,
EventIds.LocationChangeFailed,
"Location change to {URI} in circuit {CircuitId} failed.");
"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 InitializationFailed(ILogger logger, Exception exception) => _intializationFailed(logger, exception);
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 CircuitOpened(ILogger logger, string circuitId) => _onCircuitOpened(logger, circuitId, null);
public static void ConnectionUp(ILogger logger, string circuitId, string connectionId) => _onConnectionUp(logger, circuitId, connectionId, null);
public static void ConnectionDown(ILogger logger, string circuitId, string connectionId) => _onConnectionDown(logger, circuitId, connectionId, null);
public static void ConnectionDown(ILogger logger, string circuitId, string connectionId) => _onConnectionDown(logger, circuitId, connectionId, null);
public static void CircuitClosed(ILogger logger, string circuitId) => _onCircuitClosed(logger, circuitId, null);
public static void CircuitHandlerFailed(ILogger logger, CircuitHandler handler, string handlerMethod, Exception exception)
@ -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 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 EndInvokeJSFailed(ILogger logger, long asyncHandle, string arguments) => _endInvokeJSFailed(logger, asyncHandle, arguments, 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 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 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 jsRuntime = (RemoteJSRuntime)scope.ServiceProvider.GetRequiredService<IJSRuntime>();
var componentContext = (RemoteComponentContext)scope.ServiceProvider.GetRequiredService<IComponentContext>();
jsRuntime.Initialize(client);
componentContext.Initialize(client);
var navigationManager = (RemoteNavigationManager)scope.ServiceProvider.GetRequiredService<NavigationManager>();
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.
}
public Task OnRenderCompleted(long incomingBatchId, string errorMessageOrNull)
public Task OnRenderCompletedAsync(long incomingBatchId, string errorMessageOrNull)
{
if (_disposing)
{
@ -273,9 +273,9 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
if (lastBatchId < incomingBatchId)
{
HandleException(
new InvalidOperationException($"Received an acknowledgement for batch with id '{incomingBatchId}' when the last batch produced was '{lastBatchId}'."));
return Task.CompletedTask;
// This exception is due to a bad client input, so we mark it as such to prevent loging it as a warning and
// flooding the logs with warnings.
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
@ -320,7 +320,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
}
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 Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@ -203,7 +201,7 @@ namespace Microsoft.AspNetCore.Components.Server
}
Log.ReceivedConfirmationForBatch(_logger, renderId);
_ = circuitHost.Renderer.OnRenderCompleted(renderId, errorMessageOrNull);
_ = circuitHost.OnRenderCompletedAsync(renderId, errorMessageOrNull);
}
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 =
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 =
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 =
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)
{
_noComponentsRegisteredInEndpoint(logger, endpointDisplayName, null);
}
public static void NoComponentsRegisteredInEndpoint(ILogger logger, string endpointDisplayName) => _noComponentsRegisteredInEndpoint(logger, endpointDisplayName, null);
public static void ReceivedConfirmationForBatch(ILogger logger, long batchId)
{
_receivedConfirmationForBatch(logger, batchId, null);
}
public static void UnhandledExceptionInCircuit(ILogger logger, string circuitId, Exception exception)
{
_unhandledExceptionInCircuit(logger, circuitId, exception);
}
public static void ReceivedConfirmationForBatch(ILogger logger, long batchId) => _receivedConfirmationForBatch(logger, batchId, 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 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 CircuitInitializationFailed(ILogger logger, Exception exception) => _circuitInitializationFailed(logger, exception);

View File

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

View File

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

View File

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

View File

@ -31,12 +31,18 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
];
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.button = this.modal.querySelector('button')!;
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());
}
@ -57,7 +63,7 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
failed(): void {
this.button.style.display = 'block';
this.reloadParagraph.style.display = 'block';
this.message.textContent = 'Failed to reconnect to the server.';
this.reloadParagraph.style.display = 'none';
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();
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');
});

View File

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

View File

@ -6,9 +6,12 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using System.Text.RegularExpressions;
using System.Diagnostics;
using System.Text.Json;
using System.Threading.Tasks;
using Ignitor;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@ -20,7 +23,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
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;
public ComponentHubReliabilityTest(AspNetSiteServerFixture serverFixture, ITestOutputHelper output)
@ -52,7 +55,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
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.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"));
}
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]
public async Task CannotInvokeOnRenderCompletedBeforeInitialization()
{
@ -249,7 +364,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
public async Task OnLocationChanged_ReportsDebugForExceptionInValidation()
{
// 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 baseUri = new Uri(rootUri, "/subdir");
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.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()
{
// 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 baseUri = new Uri(rootUri, "/subdir");
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(
"OnLocationChanged",
new Uri(baseUri, "/test").AbsoluteUri,
false));
false));
// Assert
var actualError = Assert.Single(Errors);
Assert.Matches(expectedError, actualError);
Assert.Equal(expectedError, actualError);
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;
}
private string Serialize<T>(T browserEventDescriptor) =>
JsonSerializer.Serialize(browserEventDescriptor, TestJsonSerializerOptionsProvider.Options);
[DebuggerDisplay("{LogLevel.ToString(),nq} - {Message ?? \"null\",nq} - {Exception?.Message,nq}")]
private class LogMessage
{
public LogMessage(LogLevel logLevel, string message, Exception exception)
@ -394,7 +519,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
private class Batch
{
public Batch(int id, byte [] data)
public Batch(int id, byte[] data)
{
Id = id;
Data = data;

View File

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

View File

@ -33,7 +33,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
appElement.FindElement(By.Id("run-without-dispatch")).Click();
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);
}
}

View File

@ -1,6 +1,6 @@
<h1>Counter</h1>
<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>
<input type="checkbox" @bind="handleClicks" />

View File

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

View File

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

View File

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

View File

@ -3,10 +3,10 @@
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.Extensions.Logging;
@ -51,8 +51,6 @@ namespace Microsoft.AspNetCore.Routing
if (endpoint != null)
{
Log.MatchSkipped(_logger, endpoint);
// Someone else set the endpoint, we'll let them handle the clearing of the endpoint.
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
var endpoint = httpContext.GetEndpoint();
@ -108,18 +107,7 @@ namespace Microsoft.AspNetCore.Routing
Log.MatchSuccess(_logger, endpoint);
}
try
{
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();
}
return _next(httpContext);
}
// Initialization is async to avoid blocking threads while reflection and things

View File

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

View File

@ -21,72 +21,7 @@ namespace Microsoft.AspNetCore.Routing
public class EndpointRoutingMiddlewareTest
{
[Fact]
public async Task Invoke_ChangedPath_ResultsInDifferentResult()
{
// 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()
public async Task Invoke_OnCall_SetsEndpointFeature()
{
// Arrange
var httpContext = CreateHttpContext();
@ -99,36 +34,14 @@ namespace Microsoft.AspNetCore.Routing
// Assert
var endpointFeature = httpContext.Features.Get<IEndpointFeature>();
Assert.NotNull(endpointFeature);
Assert.Null(endpointFeature.Endpoint);
}
[Fact]
public async Task Invoke_OnCall_SetsEndpointFeatureAndResetsRouteValues()
public async Task Invoke_SkipsRouting_IfEndpointSet()
{
// Arrange
var httpContext = CreateHttpContext();
var initialRouteData = new RouteData();
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);
httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(), "myapp"));
var middleware = CreateMiddleware();
@ -137,7 +50,7 @@ namespace Microsoft.AspNetCore.Routing
// Assert
var endpoint = httpContext.GetEndpoint();
Assert.Same(expectedEndpoint, endpoint);
Assert.NotNull(endpoint);
Assert.Equal("myapp", endpoint.DisplayName);
}
@ -182,29 +95,22 @@ namespace Microsoft.AspNetCore.Routing
{
// Arrange
var httpContext = CreateHttpContext();
var nextCalled = false;
var middleware = CreateMiddleware(next: context =>
{
var routeData = httpContext.GetRouteData();
var routeValue = httpContext.GetRouteValue("controller");
var routeValuesFeature = httpContext.Features.Get<IRouteValuesFeature>();
nextCalled = true;
var middleware = CreateMiddleware();
// 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"]);
return Task.CompletedTask;
});
// Act & Assert
// Act
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]
@ -212,29 +118,22 @@ namespace Microsoft.AspNetCore.Routing
{
// Arrange
var httpContext = CreateHttpContext();
var called = false;
var middleware = CreateMiddleware(next: context =>
{
var routeData = httpContext.GetRouteData();
var routeValue = httpContext.GetRouteValue("controller");
var routeValuesFeature = httpContext.Features.Get<IRouteValuesFeature>();
called = true;
var middleware = CreateMiddleware();
// 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"]);
return Task.CompletedTask;
});
// Act & Assert
// Act
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]

View File

@ -7,6 +7,7 @@ using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
@ -103,7 +104,8 @@ namespace Microsoft.AspNetCore.Diagnostics
}
try
{
context.Response.Clear();
ClearHttpContext(context);
var exceptionHandlerFeature = new ExceptionHandlerFeature()
{
Error = edi.SourceException,
@ -137,6 +139,17 @@ namespace Microsoft.AspNetCore.Diagnostics
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)
{
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<NavigationManager, HttpNavigationManager>();
services.TryAddScoped<IJSRuntime, UnsupportedJavaScriptRuntime>();
services.TryAddScoped<IComponentContext, UnsupportedComponentContext>();
services.TryAddScoped<INavigationInterception, UnsupportedNavigationInterception>();
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>
<IsTestProject>false</IsTestProject>
<IsShippingPackage>true</IsShippingPackage>
<PackOnBuild>true</PackOnBuild>
</PropertyGroup>
<ItemGroup>

View File

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