diff --git a/Blazor.sln b/Blazor.sln index 4171202807..74c6b4b8f0 100644 --- a/Blazor.sln +++ b/Blazor.sln @@ -95,11 +95,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.Analyzers.Test", "test\Microsoft.AspNetCore.Blazor.Analyzers.Test\Microsoft.AspNetCore.Blazor.Analyzers.Test.csproj", "{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.JSInterop", "src\Microsoft.JSInterop\Microsoft.JSInterop.csproj", "{C866B19D-AFFF-45B7-8DAB-71805F39D516}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.JSInterop", "src\Microsoft.JSInterop\Microsoft.JSInterop.csproj", "{C866B19D-AFFF-45B7-8DAB-71805F39D516}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.JSInterop.Test", "test\Microsoft.JSInterop.Test\Microsoft.JSInterop.Test.csproj", "{BA1CE1FD-89D8-423F-A21B-6B212674EB39}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.JSInterop.Test", "test\Microsoft.JSInterop.Test\Microsoft.JSInterop.Test.csproj", "{BA1CE1FD-89D8-423F-A21B-6B212674EB39}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mono.WebAssembly.Interop", "src\Mono.WebAssembly.Interop\Mono.WebAssembly.Interop.csproj", "{C56873E6-8F49-476E-AF51-B5D187832CF5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mono.WebAssembly.Interop", "src\Mono.WebAssembly.Interop\Mono.WebAssembly.Interop.csproj", "{C56873E6-8F49-476E-AF51-B5D187832CF5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspnetCore.Blazor.Server.Test", "test\Microsoft.AspnetCore.Blazor.Server.Test\Microsoft.AspnetCore.Blazor.Server.Test.csproj", "{142AA6BC-5110-486B-A34D-6878E5E2CE95}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -368,6 +370,14 @@ Global {C56873E6-8F49-476E-AF51-B5D187832CF5}.Release|Any CPU.Build.0 = Release|Any CPU {C56873E6-8F49-476E-AF51-B5D187832CF5}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU {C56873E6-8F49-476E-AF51-B5D187832CF5}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU + {142AA6BC-5110-486B-A34D-6878E5E2CE95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {142AA6BC-5110-486B-A34D-6878E5E2CE95}.Debug|Any CPU.Build.0 = Debug|Any CPU + {142AA6BC-5110-486B-A34D-6878E5E2CE95}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU + {142AA6BC-5110-486B-A34D-6878E5E2CE95}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU + {142AA6BC-5110-486B-A34D-6878E5E2CE95}.Release|Any CPU.ActiveCfg = Release|Any CPU + {142AA6BC-5110-486B-A34D-6878E5E2CE95}.Release|Any CPU.Build.0 = Release|Any CPU + {142AA6BC-5110-486B-A34D-6878E5E2CE95}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU + {142AA6BC-5110-486B-A34D-6878E5E2CE95}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -413,6 +423,7 @@ Global {C866B19D-AFFF-45B7-8DAB-71805F39D516} = {B867E038-B3CE-43E3-9292-61568C46CDEB} {BA1CE1FD-89D8-423F-A21B-6B212674EB39} = {ADA3AE29-F6DE-49F6-8C7C-B321508CAE8E} {C56873E6-8F49-476E-AF51-B5D187832CF5} = {7B5CAAB1-A3EB-44F7-87E3-A13ED89FC17D} + {142AA6BC-5110-486B-A34D-6878E5E2CE95} = {ADA3AE29-F6DE-49F6-8C7C-B321508CAE8E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {504DA352-6788-4DC0-8705-82167E72A4D3} diff --git a/src/Microsoft.AspNetCore.Blazor.Server/Circuits/CircuitSynchronizationContext.cs b/src/Microsoft.AspNetCore.Blazor.Server/Circuits/CircuitSynchronizationContext.cs new file mode 100644 index 0000000000..dc62008f11 --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor.Server/Circuits/CircuitSynchronizationContext.cs @@ -0,0 +1,269 @@ +// 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.Collections.Concurrent; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Blazor.Server.Circuits +{ + [DebuggerDisplay("{_state,nq}")] + internal class CircuitSynchronizationContext : SynchronizationContext + { + private static readonly ContextCallback ExecutionContextThunk = (object state) => + { + var item = (WorkItem)state; + item.SynchronizationContext.ExecuteSynchronously(null, item.Callback, item.State); + }; + + private static readonly Action BackgroundWorkThunk = (Task task, object state) => + { + var item = (WorkItem)state; + item.SynchronizationContext.ExecuteBackground(item); + }; + + private readonly State _state; + + public event UnhandledExceptionEventHandler UnhandledException; + + public CircuitSynchronizationContext() + : this(new State()) + { + } + + private CircuitSynchronizationContext(State state) + { + _state = state; + } + + public Task Invoke(Action action) + { + var completion = new TaskCompletionSource(); + Post(_ => + { + try + { + action(); + completion.SetResult(null); + } + catch (Exception exception) + { + completion.SetException(exception); + } + }, null); + + return completion.Task; + } + + public Task InvokeAsync(Func asyncAction) + { + var completion = new TaskCompletionSource(); + Post(async (_) => + { + try + { + await asyncAction(); + completion.SetResult(null); + } + catch (Exception exception) + { + completion.SetException(exception); + } + }, null); + + return completion.Task; + } + + public Task Invoke(Func function) + { + var completion = new TaskCompletionSource(); + Post(_ => + { + try + { + var result = function(); + completion.SetResult(result); + } + catch (Exception exception) + { + completion.SetException(exception); + } + }, null); + + return completion.Task; + } + + public Task InvokeAsync(Func> asyncFunction) + { + var completion = new TaskCompletionSource(); + Post(async (_) => + { + try + { + var result = await asyncFunction(); + completion.SetResult(result); + } + catch (Exception exception) + { + completion.SetException(exception); + } + }, null); + + return completion.Task; + } + + // asynchronously runs the callback + public override void Post(SendOrPostCallback d, object state) + { + TaskCompletionSource completion; + lock (_state.Lock) + { + if (!_state.Task.IsCompleted) + { + _state.Task = Enqueue(_state.Task, d, state); + return; + } + + // We can execute this synchronously because nothing is currently running + // or queued. + completion = new TaskCompletionSource(); + _state.Task = completion.Task; + } + + ExecuteSynchronously(completion, d, state); + } + + // synchronously runs the callback + public override void Send(SendOrPostCallback d, object state) + { + Task antecedant; + var completion = new TaskCompletionSource(); + + lock (_state.Lock) + { + antecedant = _state.Task; + _state.Task = completion.Task; + } + + // We have to block. That's the contract of Send - we don't expect this to be used + // in many scenarios in Blazor. + // + // Using Wait here is ok because the antecedant task will never throw. + antecedant.Wait(); + + ExecuteSynchronously(completion, d, state); + } + + // shallow copy + public override SynchronizationContext CreateCopy() + { + return new CircuitSynchronizationContext(_state); + } + + private Task Enqueue(Task antecedant, SendOrPostCallback d, object state) + { + // If we get here is means that a callback is being queued while something is currently executing + // in this context. Let's instead add it to the queue and yield. + // + // We use our own queue here to maintain the execution order of the callbacks scheduled here. Also + // we need a queue rather than just scheduling an item in the thread pool - those items would immediately + // block and hurt scalability. + // + // We need to capture the execution context so we can restore it later. This code is similar to + // the call path of ThreadPool.QueueUserWorkItem and System.Threading.QueueUserWorkItemCallback. + ExecutionContext executionContext = null; + if (!ExecutionContext.IsFlowSuppressed()) + { + executionContext = ExecutionContext.Capture(); + } + + return antecedant.ContinueWith(BackgroundWorkThunk, new WorkItem() + { + SynchronizationContext = this, + ExecutionContext = executionContext, + Callback = d, + State = state, + }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Current); + } + + private void ExecuteSynchronously( + TaskCompletionSource completion, + SendOrPostCallback d, + object state) + { + var original = Current; + try + { + SetSynchronizationContext(this); + _state.IsBusy = true; + + d(state); + } + finally + { + _state.IsBusy = false; + SetSynchronizationContext(original); + + completion?.SetResult(null); + } + } + + private void ExecuteBackground(WorkItem item) + { + if (item.ExecutionContext == null) + { + try + { + ExecuteSynchronously(null, item.Callback, item.State); + } + catch (Exception ex) + { + DispatchException(ex); + } + + return; + } + + // Perf - using a static thunk here to avoid a delegate allocation. + try + { + ExecutionContext.Run(item.ExecutionContext, ExecutionContextThunk, item); + } + catch (Exception ex) + { + DispatchException(ex); + } + } + + private void DispatchException(Exception ex) + { + var handler = UnhandledException; + if (handler != null) + { + handler(this, new UnhandledExceptionEventArgs(ex, isTerminating: false)); + } + } + + private class State + { + public bool IsBusy; // Just for debugging + public object Lock = new object(); + public Task Task = Task.CompletedTask; + + public override string ToString() + { + return $"{{ Busy: {IsBusy}, Pending Task: {Task} }}"; + } + } + + private class WorkItem + { + public CircuitSynchronizationContext SynchronizationContext; + public ExecutionContext ExecutionContext; + public SendOrPostCallback Callback; + public object State; + } + } +} diff --git a/src/Microsoft.AspNetCore.Blazor.Server/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Blazor.Server/Properties/AssemblyInfo.cs index 1f2a6fd156..cf3e287297 100644 --- a/src/Microsoft.AspNetCore.Blazor.Server/Properties/AssemblyInfo.cs +++ b/src/Microsoft.AspNetCore.Blazor.Server/Properties/AssemblyInfo.cs @@ -1,3 +1,4 @@ -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Cli")] +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Server.Test")] diff --git a/test/Microsoft.AspnetCore.Blazor.Server.Test/Circuits/CircuitSynchronizationContextTest.cs b/test/Microsoft.AspnetCore.Blazor.Server.Test/Circuits/CircuitSynchronizationContextTest.cs new file mode 100644 index 0000000000..ff9352c9aa --- /dev/null +++ b/test/Microsoft.AspnetCore.Blazor.Server.Test/Circuits/CircuitSynchronizationContextTest.cs @@ -0,0 +1,683 @@ +// 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.Globalization; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Blazor.Server.Circuits; +using Xunit; + +namespace Microsoft.AspnetCore.Blazor.Server +{ + public class CircuitSynchronizationContextTest + { + // Nothing should exceed the timeout in a successful run of the the tests, this is just here to catch + // failures. + public TimeSpan Timeout = Debugger.IsAttached ? System.Threading.Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(10); + + [Fact] + public void Post_CanRunSynchronously_WhenNotBusy() + { + // Arrange + var context = new CircuitSynchronizationContext(); + var thread = Thread.CurrentThread; + Thread capturedThread = null; + + // Act + context.Post((_) => + { + capturedThread = Thread.CurrentThread; + }, null); + + // Assert + Assert.Same(thread, capturedThread); + } + + [Fact] + public void Post_CanRunSynchronously_WhenNotBusy_Exception() + { + // Arrange + var context = new CircuitSynchronizationContext(); + + // Act & Assert + Assert.Throws(() => context.Post((_) => + { + throw new InvalidTimeZoneException(); + }, null)); + } + + [Fact] + public async Task Post_CanRunAsynchronously_WhenBusy() + { + // Arrange + var context = new CircuitSynchronizationContext(); + var thread = Thread.CurrentThread; + Thread capturedThread = null; + + var e1 = new ManualResetEventSlim(); + var e2 = new ManualResetEventSlim(); + var e3 = new ManualResetEventSlim(); + + var task = Task.Run(() => + { + context.Send((_) => + { + e1.Set(); + Assert.True(e2.Wait(Timeout), "timeout"); + }, null); + }); + + Assert.True(e1.Wait(Timeout), "timeout"); + + // Act + context.Post((_) => + { + capturedThread = Thread.CurrentThread; + + e3.Set(); + }, null); + + // Assert + Assert.False(e2.IsSet); + e2.Set(); // Unblock the first item + await task; + + Assert.True(e3.Wait(Timeout), "timeout"); + Assert.NotSame(thread, capturedThread); + } + + [Fact] + public async Task Post_CanRunAsynchronously_CaptureExecutionContext() + { + // Arrange + var context = new CircuitSynchronizationContext(); + + // CultureInfo uses the execution context. + CultureInfo.CurrentCulture = new CultureInfo("en-GB"); + CultureInfo capturedCulture = null; + + SynchronizationContext capturedContext = null; + + var e1 = new ManualResetEventSlim(); + var e2 = new ManualResetEventSlim(); + var e3 = new ManualResetEventSlim(); + + var task = Task.Run(() => + { + context.Send((_) => + { + e1.Set(); + Assert.True(e2.Wait(Timeout), "timeout"); + }, null); + }); + + Assert.True(e1.Wait(Timeout), "timeout"); + + // Act + SynchronizationContext original = SynchronizationContext.Current; + + try + { + SynchronizationContext.SetSynchronizationContext(context); + context.Post((_) => + { + capturedCulture = CultureInfo.CurrentCulture; + capturedContext = SynchronizationContext.Current; + e3.Set(); + }, null); + } + finally + { + SynchronizationContext.SetSynchronizationContext(original); + } + + // Assert + Assert.False(e2.IsSet); + e2.Set(); // Unblock the first item + await task; + + Assert.True(e3.Wait(Timeout), "timeout"); + Assert.Same(CultureInfo.CurrentCulture, capturedCulture); + Assert.Same(context, capturedContext); + } + + [Fact] + public async Task Post_CanRunAsynchronously_WhenBusy_Exception() + { + // Arrange + var context = new CircuitSynchronizationContext(); + + Exception exception = null; + context.UnhandledException += (sender, e) => + { + exception = (InvalidTimeZoneException)e.ExceptionObject; + }; + + var e1 = new ManualResetEventSlim(); + var e2 = new ManualResetEventSlim(); + + var task = Task.Run(() => + { + context.Send((_) => + { + e1.Set(); + Assert.True(e2.Wait(Timeout), "timeout"); + }, null); + }); + + Assert.True(e1.Wait(Timeout), "timeout"); + + // Act + context.Post((_) => + { + throw new InvalidTimeZoneException(); + }, null); + + // Assert + Assert.False(e2.IsSet); + e2.Set(); // Unblock the first item + await task; + + // Use another item to 'push through' the throwing one + context.Send((_) => { }, null); + Assert.NotNull(exception); + } + + [Fact] + public async Task Post_BackgroundWorkItem_CanProcessMoreItemsInline() + { + // Arrange + var context = new CircuitSynchronizationContext(); + Thread capturedThread = null; + + var e1 = new ManualResetEventSlim(); + var e2 = new ManualResetEventSlim(); + var e3 = new ManualResetEventSlim(); + var e4 = new ManualResetEventSlim(); + var e5 = new ManualResetEventSlim(); + var e6 = new ManualResetEventSlim(); + + // Force task2 to execute in the background + var task1 = Task.Run(() => context.Send((_) => + { + e1.Set(); + Assert.True(e2.Wait(Timeout), "timeout"); + }, null)); + + Assert.True(e1.Wait(Timeout), "timeout"); + + var task2 = Task.Run(() => + { + context.Send((_) => + { + e3.Set(); + Assert.True(e4.Wait(Timeout), "timeout"); + capturedThread = Thread.CurrentThread; + }, null); + }); + + e2.Set(); + await task1; + + Assert.True(e3.Wait(Timeout), "timeout"); + + // Act + // + // Now task2 is 'running' in the sync context. Schedule more work items - they will be + // run immediately after the second item + context.Post((_) => + { + e5.Set(); + Assert.Same(Thread.CurrentThread, capturedThread); + }, null); + context.Post((_) => + { + e6.Set(); + Assert.Same(Thread.CurrentThread, capturedThread); + }, null); + + + // Assert + e4.Set(); + await task2; + + Assert.True(e5.Wait(Timeout), "timeout"); + Assert.True(e6.Wait(Timeout), "timeout"); + } + + [Fact] + public void Post_CapturesContext() + { + // Arrange + var context = new CircuitSynchronizationContext(); + + var e1 = new ManualResetEventSlim(); + + // CultureInfo uses the execution context. + CultureInfo.CurrentCulture = new CultureInfo("en-GB"); + CultureInfo capturedCulture = null; + + SynchronizationContext capturedContext = null; + + // Act + context.Post(async (_) => + { + await Task.Yield(); + + capturedCulture = CultureInfo.CurrentCulture; + capturedContext = SynchronizationContext.Current; + e1.Set(); + }, null); + + // Assert + Assert.True(e1.Wait(Timeout), "timeout"); + Assert.Same(CultureInfo.CurrentCulture, capturedCulture); + Assert.Same(context, capturedContext); + } + + [Fact] + public void Send_CanRunSynchonously() + { + // Arrange + var context = new CircuitSynchronizationContext(); + var thread = Thread.CurrentThread; + Thread capturedThread = null; + + // Act + context.Send((_) => + { + capturedThread = Thread.CurrentThread; + }, null); + + // Assert + Assert.Same(thread, capturedThread); + } + + [Fact] + public void Send_CanRunSynchronously_Exception() + { + // Arrange + var context = new CircuitSynchronizationContext(); + + // Act & Assert + Assert.Throws(() => context.Send((_) => + { + throw new InvalidTimeZoneException(); + }, null)); + } + + [Fact] + public async Task Send_BlocksWhenOtherWorkRunning() + { + // Arrange + var context = new CircuitSynchronizationContext(); + + var e1 = new ManualResetEventSlim(); + var e2 = new ManualResetEventSlim(); + var e3 = new ManualResetEventSlim(); + var e4 = new ManualResetEventSlim(); + + // Force task2 to execute in the background + var task1 = Task.Run(() => + { + context.Send((_) => + { + e1.Set(); + Assert.True(e2.Wait(Timeout), "timeout"); + }, null); + }); + + Assert.True(e1.Wait(Timeout), "timeout"); + + // Act + // + // Dispatch this on the background thread because otherwise it would block. + var task2 = Task.Run(() => + { + e3.Set(); + context.Send((_) => + { + e4.Set(); + }, null); + }); + + // Assert + Assert.True(e3.Wait(Timeout), "timeout"); + Assert.True(e3.IsSet); + + // Unblock the first item + e2.Set(); + await task1; + + await task2; + Assert.True(e4.IsSet); + } + + [Fact] + public void Send_CapturesContext() + { + // Arrange + var context = new CircuitSynchronizationContext(); + + var e1 = new ManualResetEventSlim(); + + // CultureInfo uses the execution context. + CultureInfo.CurrentCulture = new CultureInfo("en-GB"); + CultureInfo capturedCulture = null; + + SynchronizationContext capturedContext = null; + + // Act + context.Send(async (_) => + { + await Task.Yield(); + + capturedCulture = CultureInfo.CurrentCulture; + capturedContext = SynchronizationContext.Current; + + e1.Set(); + }, null); + + // Assert + Assert.True(e1.Wait(Timeout), "timeout"); + Assert.Same(CultureInfo.CurrentCulture, capturedCulture); + Assert.Same(context, capturedContext); + } + + [Fact] + public async Task Invoke_Void_CanRunSynchronously_WhenNotBusy() + { + // Arrange + var context = new CircuitSynchronizationContext(); + var thread = Thread.CurrentThread; + Thread capturedThread = null; + + // Act + var task = context.Invoke(() => + { + capturedThread = Thread.CurrentThread; + }); + + // Assert + await task; + Assert.Same(thread, capturedThread); + } + + [Fact] + public async Task Invoke_Void_CanRunAsynchronously_WhenBusy() + { + // Arrange + var context = new CircuitSynchronizationContext(); + var thread = Thread.CurrentThread; + Thread capturedThread = null; + + var e1 = new ManualResetEventSlim(); + var e2 = new ManualResetEventSlim(); + var e3 = new ManualResetEventSlim(); + + var task1 = Task.Run(() => + { + context.Send((_) => + { + e1.Set(); + Assert.True(e2.Wait(Timeout), "timeout"); + }, null); + }); + + Assert.True(e1.Wait(Timeout), "timeout"); + + var task2 = context.Invoke(() => + { + capturedThread = Thread.CurrentThread; + + e3.Set(); + }); + + // Assert + Assert.False(e2.IsSet); + e2.Set(); // Unblock the first item + await task1; + + Assert.True(e3.Wait(Timeout), "timeout"); + await task2; + Assert.NotSame(thread, capturedThread); + } + + [Fact] + public async Task Invoke_Void_CanRethrowExceptions() + { + // Arrange + var context = new CircuitSynchronizationContext(); + + // Act + var task = context.Invoke(() => + { + throw new InvalidTimeZoneException(); + }); + + // Assert + await Assert.ThrowsAsync(async () => await task); + } + + [Fact] + public async Task Invoke_T_CanRunSynchronously_WhenNotBusy() + { + // Arrange + var context = new CircuitSynchronizationContext(); + var thread = Thread.CurrentThread; + + // Act + var task = context.Invoke(() => + { + return Thread.CurrentThread; + }); + + // Assert + Assert.Same(thread, await task); + } + + [Fact] + public async Task Invoke_T_CanRunAsynchronously_WhenBusy() + { + // Arrange + var context = new CircuitSynchronizationContext(); + var thread = Thread.CurrentThread; + + var e1 = new ManualResetEventSlim(); + var e2 = new ManualResetEventSlim(); + var e3 = new ManualResetEventSlim(); + + var task1 = Task.Run(() => + { + context.Send((_) => + { + e1.Set(); + Assert.True(e2.Wait(Timeout), "timeout"); + }, null); + }); + + Assert.True(e1.Wait(Timeout), "timeout"); + + var task2 = context.Invoke(() => + { + e3.Set(); + + return Thread.CurrentThread; + }); + + // Assert + Assert.False(e2.IsSet); + e2.Set(); // Unblock the first item + await task1; + + Assert.True(e3.Wait(Timeout), "timeout"); + Assert.NotSame(thread, await task2); + } + + [Fact] + public async Task Invoke_T_CanRethrowExceptions() + { + // Arrange + var context = new CircuitSynchronizationContext(); + + // Act + var task = context.Invoke(() => + { + throw new InvalidTimeZoneException(); + }); + + // Assert + await Assert.ThrowsAsync(async () => await task); + } + + [Fact] + public async Task InvokeAsync_Void_CanRunSynchronously_WhenNotBusy() + { + // Arrange + var context = new CircuitSynchronizationContext(); + var thread = Thread.CurrentThread; + Thread capturedThread = null; + + // Act + var task = context.Invoke(() => + { + capturedThread = Thread.CurrentThread; + return Task.CompletedTask; + }); + + // Assert + await task; + Assert.Same(thread, capturedThread); + } + + [Fact] + public async Task InvokeAsync_Void_CanRunAsynchronously_WhenBusy() + { + // Arrange + var context = new CircuitSynchronizationContext(); + var thread = Thread.CurrentThread; + Thread capturedThread = null; + + var e1 = new ManualResetEventSlim(); + var e2 = new ManualResetEventSlim(); + var e3 = new ManualResetEventSlim(); + + var task1 = Task.Run(() => + { + context.Send((_) => + { + e1.Set(); + Assert.True(e2.Wait(Timeout), "timeout"); + }, null); + }); + + Assert.True(e1.Wait(Timeout), "timeout"); + + var task2 = context.InvokeAsync(() => + { + capturedThread = Thread.CurrentThread; + + e3.Set(); + return Task.CompletedTask; + }); + + // Assert + Assert.False(e2.IsSet); + e2.Set(); // Unblock the first item + await task1; + + Assert.True(e3.Wait(Timeout), "timeout"); + await task2; + Assert.NotSame(thread, capturedThread); + } + + [Fact] + public async Task InvokeAsync_Void_CanRethrowExceptions() + { + // Arrange + var context = new CircuitSynchronizationContext(); + + // Act + var task = context.InvokeAsync(() => + { + throw new InvalidTimeZoneException(); + }); + + // Assert + await Assert.ThrowsAsync(async () => await task); + } + + [Fact] + public async Task InvokeAsync_T_CanRunSynchronously_WhenNotBusy() + { + // Arrange + var context = new CircuitSynchronizationContext(); + var thread = Thread.CurrentThread; + + // Act + var task = context.InvokeAsync(() => + { + return Task.FromResult(Thread.CurrentThread); + }); + + // Assert + Assert.Same(thread, await task); + } + + [Fact] + public async Task InvokeAsync_T_CanRunAsynchronously_WhenBusy() + { + // Arrange + var context = new CircuitSynchronizationContext(); + var thread = Thread.CurrentThread; + + var e1 = new ManualResetEventSlim(); + var e2 = new ManualResetEventSlim(); + var e3 = new ManualResetEventSlim(); + + var task1 = Task.Run(() => + { + context.Send((_) => + { + e1.Set(); + Assert.True(e2.Wait(Timeout), "timeout"); + }, null); + }); + + Assert.True(e1.Wait(Timeout), "timeout"); + + var task2 = context.InvokeAsync(() => + { + e3.Set(); + + return Task.FromResult(Thread.CurrentThread); + }); + + // Assert + Assert.False(e2.IsSet); + e2.Set(); // Unblock the first item + await task1; + + Assert.True(e3.Wait(Timeout), "timeout"); + Assert.NotSame(thread, await task2); + } + + [Fact] + public async Task InvokeAsync_T_CanRethrowExceptions() + { + // Arrange + var context = new CircuitSynchronizationContext(); + + // Act + var task = context.InvokeAsync(() => + { + throw new InvalidTimeZoneException(); + }); + + // Assert + await Assert.ThrowsAsync(async () => await task); + } + } +} diff --git a/test/Microsoft.AspnetCore.Blazor.Server.Test/Microsoft.AspnetCore.Blazor.Server.Test.csproj b/test/Microsoft.AspnetCore.Blazor.Server.Test/Microsoft.AspnetCore.Blazor.Server.Test.csproj new file mode 100644 index 0000000000..5d65574af4 --- /dev/null +++ b/test/Microsoft.AspnetCore.Blazor.Server.Test/Microsoft.AspnetCore.Blazor.Server.Test.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp2.0 + false + + + + + + + + + + + + + +