Implement server-side sync context

This is a synchronization context we can use for server side blazor to
support a single logical thread of execution. This is optimized for
scalability and non-blocking behavior.
This commit is contained in:
Ryan Nowak 2018-06-28 19:27:25 -07:00
parent 8724b84a14
commit e0168eb0c8
5 changed files with 987 additions and 4 deletions

View File

@ -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}

View File

@ -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<Task, object> 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<object>();
Post(_ =>
{
try
{
action();
completion.SetResult(null);
}
catch (Exception exception)
{
completion.SetException(exception);
}
}, null);
return completion.Task;
}
public Task InvokeAsync(Func<Task> asyncAction)
{
var completion = new TaskCompletionSource<object>();
Post(async (_) =>
{
try
{
await asyncAction();
completion.SetResult(null);
}
catch (Exception exception)
{
completion.SetException(exception);
}
}, null);
return completion.Task;
}
public Task<TResult> Invoke<TResult>(Func<TResult> function)
{
var completion = new TaskCompletionSource<TResult>();
Post(_ =>
{
try
{
var result = function();
completion.SetResult(result);
}
catch (Exception exception)
{
completion.SetException(exception);
}
}, null);
return completion.Task;
}
public Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> asyncFunction)
{
var completion = new TaskCompletionSource<TResult>();
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<object> 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<object>();
_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<object>();
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<object> 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;
}
}
}

View File

@ -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")]

View File

@ -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<InvalidTimeZoneException>(() => 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<InvalidTimeZoneException>(() => 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<InvalidTimeZoneException>(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<string>(() =>
{
throw new InvalidTimeZoneException();
});
// Assert
await Assert.ThrowsAsync<InvalidTimeZoneException>(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<InvalidTimeZoneException>(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<string>(() =>
{
throw new InvalidTimeZoneException();
});
// Assert
await Assert.ThrowsAsync<InvalidTimeZoneException>(async () => await task);
}
}
}

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Blazor.Server\Microsoft.AspNetCore.Blazor.Server.csproj" />
</ItemGroup>
</Project>