diff --git a/src/Components/Ignitor/src/BlazorClient.cs b/src/Components/Ignitor/src/BlazorClient.cs index 67737d5bb4..cddc48e094 100644 --- a/src/Components/Ignitor/src/BlazorClient.cs +++ b/src/Components/Ignitor/src/BlazorClient.cs @@ -13,17 +13,21 @@ using Microsoft.AspNetCore.SignalR.Protocol; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +#nullable enable namespace Ignitor { public class BlazorClient : IAsyncDisposable { private const string MarkerPattern = ".*?.*?"; + private HubConnection? _hubConnection; public BlazorClient() { CancellationTokenSource = new CancellationTokenSource(); - TaskCompletionSource = new TaskCompletionSource(); + CancellationToken = CancellationTokenSource.Token; + TaskCompletionSource = new TaskCompletionSource(); CancellationTokenSource.Token.Register(() => { @@ -44,104 +48,103 @@ namespace Ignitor /// Gets the collections of operation results that are captured when /// is true. /// - public Operations Operations { get; private set; } + public Operations Operations { get; } = new Operations(); - public Func FormatError { get; set; } + public Func? FormatError { get; set; } private CancellationTokenSource CancellationTokenSource { get; } - private CancellationToken CancellationToken => CancellationTokenSource.Token; + private CancellationToken CancellationToken { get; } - private TaskCompletionSource TaskCompletionSource { get; } + private TaskCompletionSource TaskCompletionSource { get; } - private CancellableOperation NextAttachComponentReceived { get; set; } + private CancellableOperation? NextAttachComponentReceived { get; set; } - private CancellableOperation NextBatchReceived { get; set; } + private CancellableOperation? NextBatchReceived { get; set; } - private CancellableOperation NextErrorReceived { get; set; } + private CancellableOperation? NextErrorReceived { get; set; } - private CancellableOperation NextDisconnect { get; set; } + private CancellableOperation? NextDisconnect { get; set; } - private CancellableOperation NextJSInteropReceived { get; set; } + private CancellableOperation? NextJSInteropReceived { get; set; } - private CancellableOperation NextDotNetInteropCompletionReceived { get; set; } + private CancellableOperation? NextDotNetInteropCompletionReceived { get; set; } - public ILoggerProvider LoggerProvider { get; set; } + public ILoggerProvider LoggerProvider { get; set; } = NullLoggerProvider.Instance; public bool ConfirmRenderBatch { get; set; } = true; - public event Action JSInterop; + public event Action? JSInterop; - public event Action RenderBatchReceived; + public event Action? RenderBatchReceived; - public event Action DotNetInteropCompletion; + public event Action? DotNetInteropCompletion; - public event Action OnCircuitError; + public event Action? OnCircuitError; - public string CircuitId { get; set; } + public string? CircuitId { get; private set; } - public ElementHive Hive { get; set; } = new ElementHive(); + public ElementHive Hive { get; } = new ElementHive(); public bool ImplicitWait => DefaultOperationTimeout != null; - public HubConnection HubConnection { get; private set; } + public HubConnection HubConnection => _hubConnection ?? throw new InvalidOperationException("HubConnection has not been initialized."); - public Task PrepareForNextBatch(TimeSpan? timeout) + public Task PrepareForNextBatch(TimeSpan? timeout) { if (NextBatchReceived != null && !NextBatchReceived.Disposed) { throw new InvalidOperationException("Invalid state previous task not completed"); } - NextBatchReceived = new CancellableOperation(timeout); - + NextBatchReceived = new CancellableOperation(timeout, CancellationToken); return NextBatchReceived.Completion.Task; } - public Task PrepareForNextJSInterop(TimeSpan? timeout) + public Task PrepareForNextJSInterop(TimeSpan? timeout) { if (NextJSInteropReceived != null && !NextJSInteropReceived.Disposed) { throw new InvalidOperationException("Invalid state previous task not completed"); } - NextJSInteropReceived = new CancellableOperation(timeout); + NextJSInteropReceived = new CancellableOperation(timeout, CancellationToken); return NextJSInteropReceived.Completion.Task; } - public Task PrepareForNextDotNetInterop(TimeSpan? timeout) + public Task PrepareForNextDotNetInterop(TimeSpan? timeout) { if (NextDotNetInteropCompletionReceived != null && !NextDotNetInteropCompletionReceived.Disposed) { throw new InvalidOperationException("Invalid state previous task not completed"); } - NextDotNetInteropCompletionReceived = new CancellableOperation(timeout); + NextDotNetInteropCompletionReceived = new CancellableOperation(timeout, CancellationToken); return NextDotNetInteropCompletionReceived.Completion.Task; } - public Task PrepareForNextCircuitError(TimeSpan? timeout) + public Task PrepareForNextCircuitError(TimeSpan? timeout) { if (NextErrorReceived != null && !NextErrorReceived.Disposed) { throw new InvalidOperationException("Invalid state previous task not completed"); } - NextErrorReceived = new CancellableOperation(timeout); + NextErrorReceived = new CancellableOperation(timeout, CancellationToken); return NextErrorReceived.Completion.Task; } - public Task PrepareForNextDisconnect(TimeSpan? timeout) + public Task PrepareForNextDisconnect(TimeSpan? timeout) { if (NextDisconnect != null && !NextDisconnect.Disposed) { throw new InvalidOperationException("Invalid state previous task not completed"); } - NextDisconnect = new CancellableOperation(timeout); + NextDisconnect = new CancellableOperation(timeout, CancellationToken); return NextDisconnect.Completion.Task; } @@ -172,44 +175,44 @@ namespace Ignitor return ExpectRenderBatch(() => elementNode.SelectAsync(HubConnection, value)); } - public async Task ExpectRenderBatch(Func action, TimeSpan? timeout = null) + public async Task ExpectRenderBatch(Func action, TimeSpan? timeout = null) { var task = WaitForRenderBatch(timeout); await action(); return await task; } - public async Task ExpectJSInterop(Func action, TimeSpan? timeout = null) + public async Task ExpectJSInterop(Func action, TimeSpan? timeout = null) { var task = WaitForJSInterop(timeout); await action(); return await task; } - public async Task ExpectDotNetInterop(Func action, TimeSpan? timeout = null) + public async Task ExpectDotNetInterop(Func action, TimeSpan? timeout = null) { var task = WaitForDotNetInterop(timeout); await action(); return await task; } - public async Task ExpectCircuitError(Func action, TimeSpan? timeout = null) + public async Task ExpectCircuitError(Func action, TimeSpan? timeout = null) { var task = WaitForCircuitError(timeout); await action(); return await task; } - public async Task ExpectDisconnect(Func action, TimeSpan? timeout = null) + public async Task ExpectDisconnect(Func action, TimeSpan? timeout = null) { var task = WaitForDisconnect(timeout); await action(); return await task; } - public async Task<(string error, Exception exception)> ExpectCircuitErrorAndDisconnect(Func action, TimeSpan? timeout = null) + public async Task<(string? error, Exception? exception)> ExpectCircuitErrorAndDisconnect(Func action, TimeSpan? timeout = null) { - string error = null; + string? error = default; // NOTE: timeout is used for each operation individually. var exception = await ExpectDisconnect(async () => @@ -220,7 +223,7 @@ namespace Ignitor return (error, exception); } - private async Task WaitForRenderBatch(TimeSpan? timeout = null) + private async Task WaitForRenderBatch(TimeSpan? timeout = null) { if (ImplicitWait) { @@ -233,7 +236,7 @@ namespace Ignitor { return await PrepareForNextBatch(timeout ?? DefaultOperationTimeout); } - catch (OperationCanceledException) + catch (TimeoutException) when (FormatError != null) { throw FormatError("Timed out while waiting for batch."); } @@ -242,7 +245,7 @@ namespace Ignitor return null; } - private async Task WaitForJSInterop(TimeSpan? timeout = null) + private async Task WaitForJSInterop(TimeSpan? timeout = null) { if (ImplicitWait) { @@ -255,7 +258,7 @@ namespace Ignitor { return await PrepareForNextJSInterop(timeout ?? DefaultOperationTimeout); } - catch (OperationCanceledException) + catch (TimeoutException) when (FormatError != null) { throw FormatError("Timed out while waiting for JS Interop."); } @@ -264,7 +267,7 @@ namespace Ignitor return null; } - private async Task WaitForDotNetInterop(TimeSpan? timeout = null) + private async Task WaitForDotNetInterop(TimeSpan? timeout = null) { if (ImplicitWait) { @@ -277,7 +280,7 @@ namespace Ignitor { return await PrepareForNextDotNetInterop(timeout ?? DefaultOperationTimeout); } - catch (OperationCanceledException) + catch (TimeoutException) when (FormatError != null) { throw FormatError("Timed out while waiting for .NET interop."); } @@ -286,7 +289,7 @@ namespace Ignitor return null; } - private async Task WaitForCircuitError(TimeSpan? timeout = null) + private async Task WaitForCircuitError(TimeSpan? timeout = null) { if (ImplicitWait) { @@ -299,7 +302,7 @@ namespace Ignitor { return await PrepareForNextCircuitError(timeout ?? DefaultOperationTimeout); } - catch (OperationCanceledException) + catch (TimeoutException) when (FormatError != null) { throw FormatError("Timed out while waiting for circuit error."); } @@ -308,7 +311,7 @@ namespace Ignitor return null; } - private async Task WaitForDisconnect(TimeSpan? timeout = null) + private async Task WaitForDisconnect(TimeSpan? timeout = null) { if (ImplicitWait) { @@ -321,7 +324,7 @@ namespace Ignitor { return await PrepareForNextDisconnect(timeout ?? DefaultOperationTimeout); } - catch (OperationCanceledException) + catch (TimeoutException) when (FormatError != null) { throw FormatError("Timed out while waiting for disconnect."); } @@ -344,7 +347,7 @@ namespace Ignitor } }); - HubConnection = builder.Build(); + _hubConnection = builder.Build(); await HubConnection.StartAsync(CancellationToken); HubConnection.On("JS.AttachComponent", OnAttachComponent); @@ -354,11 +357,6 @@ namespace Ignitor HubConnection.On("JS.Error", OnError); HubConnection.Closed += OnClosedAsync; - if (CaptureOperations) - { - Operations = new Operations(); - } - if (!connectAutomatically) { return true; @@ -366,7 +364,7 @@ namespace Ignitor var descriptors = await GetPrerenderDescriptors(uri); await ExpectRenderBatch( - async () => CircuitId = await HubConnection.InvokeAsync("StartCircuit", uri, uri, descriptors), + async () => CircuitId = await HubConnection.InvokeAsync("StartCircuit", uri, uri, descriptors, CancellationToken), DefaultConnectionTimeout); return CircuitId != null; } @@ -415,9 +413,9 @@ namespace Ignitor NextBatchReceived?.Completion?.TrySetResult(null); } - public Task ConfirmBatch(int batchId, string error = null) + public Task ConfirmBatch(int batchId, string? error = null) { - return HubConnection.InvokeAsync("OnRenderCompleted", batchId, error); + return HubConnection.InvokeAsync("OnRenderCompleted", batchId, error, CancellationToken); } private void OnError(string error) @@ -468,7 +466,14 @@ namespace Ignitor public async Task InvokeDotNetMethod(object callId, string assemblyName, string methodIdentifier, object dotNetObjectId, string argsJson) { - await ExpectDotNetInterop(() => HubConnection.InvokeAsync("BeginInvokeDotNetFromJS", callId?.ToString(), assemblyName, methodIdentifier, dotNetObjectId ?? 0, argsJson)); + await ExpectDotNetInterop(() => HubConnection.InvokeAsync( + "BeginInvokeDotNetFromJS", + callId?.ToString(), + assemblyName, + methodIdentifier, + dotNetObjectId ?? 0, + argsJson, + CancellationToken)); } public async Task GetPrerenderDescriptors(Uri uri) @@ -485,8 +490,11 @@ namespace Ignitor public void Cancel() { - CancellationTokenSource.Cancel(); - CancellationTokenSource.Dispose(); + if (!CancellationTokenSource.IsCancellationRequested) + { + CancellationTokenSource.Cancel(); + CancellationTokenSource.Dispose(); + } } public ElementNode FindElementById(string id) @@ -519,6 +527,7 @@ namespace Ignitor public async ValueTask DisposeAsync() { + Cancel(); if (HubConnection != null) { await HubConnection.DisposeAsync(); @@ -526,3 +535,5 @@ namespace Ignitor } } } + +#nullable restore diff --git a/src/Components/Ignitor/src/CancellableOperation.cs b/src/Components/Ignitor/src/CancellableOperation.cs index dad8cb5587..6e1f217d19 100644 --- a/src/Components/Ignitor/src/CancellableOperation.cs +++ b/src/Components/Ignitor/src/CancellableOperation.cs @@ -5,11 +5,12 @@ using System; using System.Threading; using System.Threading.Tasks; +#nullable enable namespace Ignitor { internal class CancellableOperation { - public CancellableOperation(TimeSpan? timeout) + public CancellableOperation(TimeSpan? timeout, CancellationToken cancellationToken) { Timeout = timeout; @@ -17,25 +18,37 @@ namespace Ignitor Completion.Task.ContinueWith( (task, state) => { - var operation = (CancellableOperation)state; + var operation = (CancellableOperation)state!; operation.Dispose(); }, this, TaskContinuationOptions.ExecuteSynchronously); // We need to execute synchronously to clean-up before anything else continues + + Cancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + if (Timeout != null && Timeout != System.Threading.Timeout.InfiniteTimeSpan && Timeout != TimeSpan.MaxValue) { - Cancellation = new CancellationTokenSource(Timeout.Value); - CancellationRegistration = Cancellation.Token.Register( - (self) => - { - var operation = (CancellableOperation)self; - operation.Completion.TrySetCanceled(operation.Cancellation.Token); - operation.Cancellation.Dispose(); - operation.CancellationRegistration.Dispose(); - }, - this); + Cancellation.CancelAfter(Timeout.Value); } + + CancellationRegistration = Cancellation.Token.Register( + (self) => + { + var operation = (CancellableOperation)self!; + + if (cancellationToken.IsCancellationRequested) + { + // The operation was externally canceled before it timed out. + Dispose(); + return; + } + + operation.Completion.TrySetException(new TimeoutException($"The operation timed out after {Timeout}.")); + operation.Cancellation?.Dispose(); + operation.CancellationRegistration.Dispose(); + }, + this); } public TimeSpan? Timeout { get; } @@ -62,3 +75,4 @@ namespace Ignitor } } } +#nullable restore diff --git a/src/Components/Ignitor/src/ComponentState.cs b/src/Components/Ignitor/src/ComponentState.cs index 2acae3af21..28683bee11 100644 --- a/src/Components/Ignitor/src/ComponentState.cs +++ b/src/Components/Ignitor/src/ComponentState.cs @@ -3,6 +3,7 @@ namespace Ignitor { +#nullable enable public class ComponentState { public ComponentState(int componentId) @@ -11,6 +12,7 @@ namespace Ignitor } public int ComponentId { get; } - public IComponent Component { get; } + public IComponent? Component { get; } } +#nullable restore } diff --git a/src/Components/Ignitor/src/ContainerNode.cs b/src/Components/Ignitor/src/ContainerNode.cs index 9e933dee23..24ee50b458 100644 --- a/src/Components/Ignitor/src/ContainerNode.cs +++ b/src/Components/Ignitor/src/ContainerNode.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +#nullable enable namespace Ignitor { public abstract class ContainerNode : Node @@ -82,3 +83,4 @@ namespace Ignitor } } } +#nullable restore diff --git a/src/Components/Ignitor/src/ElementHive.cs b/src/Components/Ignitor/src/ElementHive.cs index 95c4c02397..3bb37c38ce 100644 --- a/src/Components/Ignitor/src/ElementHive.cs +++ b/src/Components/Ignitor/src/ElementHive.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +#nullable enable namespace Ignitor { public class ElementHive @@ -35,7 +37,7 @@ namespace Ignitor } } - public bool TryFindElementById(string id, out ElementNode element) + public bool TryFindElementById(string id, [NotNullWhen(true)] out ElementNode? element) { foreach (var kvp in Components) { @@ -49,7 +51,7 @@ namespace Ignitor element = null; return false; - bool TryGetElementFromChildren(Node node, out ElementNode foundNode) + bool TryGetElementFromChildren(Node node, out ElementNode? foundNode) { if (node is ElementNode elementNode && elementNode.Attributes.TryGetValue("id", out var elementId) && @@ -81,6 +83,7 @@ namespace Ignitor { component = new ComponentNode(componentId); Components.Add(componentId, component); + } ApplyEdits(batch, component, 0, edits); @@ -199,7 +202,7 @@ namespace Ignitor case RenderTreeEditType.StepOut: { - parent = parent.Parent; + parent = parent.Parent ?? throw new InvalidOperationException($"Cannot step out of {parent}"); currentDepth--; childIndexAtCurrentDepth = currentDepth == 0 ? childIndex : 0; // The childIndex is only ever nonzero at zero depth break; @@ -469,3 +472,4 @@ namespace Ignitor } } } +#nullable restore diff --git a/src/Components/Ignitor/src/ElementNode.cs b/src/Components/Ignitor/src/ElementNode.cs index cebcaef323..06126cf42e 100644 --- a/src/Components/Ignitor/src/ElementNode.cs +++ b/src/Components/Ignitor/src/ElementNode.cs @@ -7,6 +7,7 @@ using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; +#nullable enable namespace Ignitor { public class ElementNode : ContainerNode @@ -87,7 +88,7 @@ namespace Ignitor return DispatchEventCore(connection, Serialize(webEventDescriptor), Serialize(args)); } - public Task ClickAsync(HubConnection connection) + internal Task ClickAsync(HubConnection connection) { if (!Events.TryGetValue("click", out var clickEventDescriptor)) { @@ -129,3 +130,4 @@ namespace Ignitor } } } +#nullable restore diff --git a/src/Components/Ignitor/src/Error.cs b/src/Components/Ignitor/src/Error.cs index e452e5d195..7258ebe885 100644 --- a/src/Components/Ignitor/src/Error.cs +++ b/src/Components/Ignitor/src/Error.cs @@ -1,10 +1,12 @@ // 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. +#nullable enable namespace Ignitor { public class Error { - public string Stack { get; set; } + public string? Stack { get; set; } } } +#nullable restore diff --git a/src/Components/Ignitor/src/IComponent.cs b/src/Components/Ignitor/src/IComponent.cs index ce9b3d324c..54b1c853a6 100644 --- a/src/Components/Ignitor/src/IComponent.cs +++ b/src/Components/Ignitor/src/IComponent.cs @@ -1,6 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Text; +// 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 Ignitor { diff --git a/src/Components/Ignitor/src/Node.cs b/src/Components/Ignitor/src/Node.cs index 07fef1bd4a..04b2559333 100644 --- a/src/Components/Ignitor/src/Node.cs +++ b/src/Components/Ignitor/src/Node.cs @@ -1,10 +1,14 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +#nullable enable + namespace Ignitor { public abstract class Node { - public virtual ContainerNode Parent { get; set; } + public virtual ContainerNode? Parent { get; set; } } } + +#nullable restore diff --git a/src/Components/Ignitor/src/NodeSerializer.cs b/src/Components/Ignitor/src/NodeSerializer.cs index b3984a319f..4d5919ce94 100644 --- a/src/Components/Ignitor/src/NodeSerializer.cs +++ b/src/Components/Ignitor/src/NodeSerializer.cs @@ -4,6 +4,8 @@ using System; using System.IO; +#nullable enable + namespace Ignitor { internal static class NodeSerializer @@ -96,7 +98,7 @@ namespace Ignitor if (attribute.Value != null) { Write("=\""); - Write(attribute.Value.ToString()); + Write(attribute.Value.ToString()!); Write("\""); } } @@ -113,7 +115,7 @@ namespace Ignitor if (properties.Value != null) { Write("=\""); - Write(properties.Value.ToString()); + Write(properties.Value.ToString()!); Write("\""); } } @@ -194,3 +196,4 @@ namespace Ignitor } } } +#nullable restore diff --git a/src/Components/Ignitor/src/Operations.cs b/src/Components/Ignitor/src/Operations.cs index efcb5346fb..e9c525835c 100644 --- a/src/Components/Ignitor/src/Operations.cs +++ b/src/Components/Ignitor/src/Operations.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; +#nullable enable namespace Ignitor { public sealed class Operations @@ -18,3 +19,4 @@ namespace Ignitor public ConcurrentQueue JSInteropCalls { get; } = new ConcurrentQueue(); } } +#nullable restore diff --git a/src/Components/Ignitor/src/RenderBatchReader.cs b/src/Components/Ignitor/src/RenderBatchReader.cs index 1581fc7022..85f6c523f0 100644 --- a/src/Components/Ignitor/src/RenderBatchReader.cs +++ b/src/Components/Ignitor/src/RenderBatchReader.cs @@ -4,6 +4,8 @@ using System; using System.Text; +#nullable enable + namespace Ignitor { public static class RenderBatchReader @@ -206,7 +208,7 @@ namespace Ignitor return new ArrayRange(Array.Empty(), 0); } - private static string ReadString(ReadOnlySpan data, string[] strings) + private static string? ReadString(ReadOnlySpan data, string[] strings) { var index = BitConverter.ToInt32(data.Slice(0, 4)); return index >= 0 ? strings[index] : null; @@ -279,3 +281,4 @@ namespace Ignitor } } } +#nullable restore