diff --git a/src/Components/Components/src/BindConverter.cs b/src/Components/Components/src/BindConverter.cs index 223681ad0d..4c72cb7285 100644 --- a/src/Components/Components/src/BindConverter.cs +++ b/src/Components/Components/src/BindConverter.cs @@ -25,8 +25,8 @@ namespace Microsoft.AspNetCore.Components private delegate object? BindFormatter(T value, CultureInfo? culture); private delegate object BindFormatterWithFormat(T value, CultureInfo? culture, string format); - internal delegate bool BindParser(object? obj, CultureInfo? culture, out T value); - internal delegate bool BindParserWithFormat(object? obj, CultureInfo? culture, string? format, out T value); + internal delegate bool BindParser(object? obj, CultureInfo? culture, [MaybeNullWhen(false)] out T value); + internal delegate bool BindParserWithFormat(object? obj, CultureInfo? culture, string? format, [MaybeNullWhen(false)] out T value); /// /// Formats the provided as a . @@ -1276,7 +1276,7 @@ namespace Microsoft.AspNetCore.Components /// The to use for conversion. /// The converted value. /// true if conversion is successful, otherwise false. - public static bool TryConvertTo(object? obj, CultureInfo? culture, out T value) + public static bool TryConvertTo(object? obj, CultureInfo? culture, [MaybeNullWhen(false)] out T value) { var converter = ParserDelegateCache.Get(); return converter(obj, culture, out value); diff --git a/src/Components/Components/src/EventCallbackOfT.cs b/src/Components/Components/src/EventCallbackOfT.cs index 23f7888826..e228d8e31e 100644 --- a/src/Components/Components/src/EventCallbackOfT.cs +++ b/src/Components/Components/src/EventCallbackOfT.cs @@ -46,11 +46,11 @@ namespace Microsoft.AspNetCore.Components /// /// The argument. /// A which completes asynchronously once event processing has completed. - public Task InvokeAsync(TValue arg) + public Task InvokeAsync(TValue? arg) { if (Receiver == null) { - return EventCallbackWorkItem.InvokeAsync(Delegate, arg); + return EventCallbackWorkItem.InvokeAsync(Delegate, arg); } return Receiver.HandleEventAsync(new EventCallbackWorkItem(Delegate), arg); diff --git a/src/Components/Components/src/ParameterView.cs b/src/Components/Components/src/ParameterView.cs index f68fb60573..b58e92e9c0 100644 --- a/src/Components/Components/src/ParameterView.cs +++ b/src/Components/Components/src/ParameterView.cs @@ -88,9 +88,8 @@ namespace Microsoft.AspNetCore.Components /// The type of the value. /// The name of the parameter. /// The parameter value if found; otherwise the default value for the specified type. - [return: MaybeNull] - public TValue GetValueOrDefault(string parameterName) - => GetValueOrDefault(parameterName, default!); + public TValue? GetValueOrDefault(string parameterName) + => GetValueOrDefault(parameterName, default); /// /// Gets the value of the parameter with the specified name, or a specified default value diff --git a/src/Components/Server/src/Circuits/CircuitId.cs b/src/Components/Server/src/Circuits/CircuitId.cs index 3d63d2fdb4..90240b620b 100644 --- a/src/Components/Server/src/Circuits/CircuitId.cs +++ b/src/Components/Server/src/Circuits/CircuitId.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using System.Security.Cryptography; @@ -29,7 +28,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits public string Secret { get; } - public bool Equals([AllowNull] CircuitId other) + public bool Equals(CircuitId other) { // We want to use a fixed time equality comparison for a *real* comparisons. // The only use case for Secret being null is with a default struct value, diff --git a/src/Components/Web/src/Forms/InputBase.cs b/src/Components/Web/src/Forms/InputBase.cs index 08fd03c92a..d7793583d2 100644 --- a/src/Components/Web/src/Forms/InputBase.cs +++ b/src/Components/Web/src/Forms/InputBase.cs @@ -35,10 +35,8 @@ namespace Microsoft.AspNetCore.Components.Forms /// /// @bind-Value="model.PropertyName" /// - [AllowNull] - [MaybeNull] [Parameter] - public TValue Value { get; set; } = default; + public TValue? Value { get; set; } /// /// Gets or sets a callback that updates the bound value. @@ -69,17 +67,15 @@ namespace Microsoft.AspNetCore.Components.Forms /// /// Gets or sets the current value of the input. /// - [AllowNull] - protected TValue CurrentValue + protected TValue? CurrentValue { - [return: MaybeNull] - get => Value!; + get => Value; set { var hasChanged = !EqualityComparer.Default.Equals(value, Value); if (hasChanged) { - Value = value!; + Value = value; _ = ValueChanged.InvokeAsync(Value); EditContext.NotifyFieldChanged(FieldIdentifier); } @@ -148,7 +144,7 @@ namespace Microsoft.AspNetCore.Components.Forms /// /// The value to format. /// A string representation of the value. - protected virtual string? FormatValueAsString([AllowNull] TValue value) + protected virtual string? FormatValueAsString(TValue? value) => value?.ToString(); /// @@ -159,7 +155,7 @@ namespace Microsoft.AspNetCore.Components.Forms /// An instance of . /// If the value could not be parsed, provides a validation error message. /// True if the value could be parsed; otherwise false. - protected abstract bool TryParseValueFromString(string? value, [MaybeNull] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage); + protected abstract bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage); /// /// Gets a string that indicates the status of the field being edited. This will include diff --git a/src/Components/Web/src/Forms/InputDate.cs b/src/Components/Web/src/Forms/InputDate.cs index 4372646c7b..9b180f76e2 100644 --- a/src/Components/Web/src/Forms/InputDate.cs +++ b/src/Components/Web/src/Forms/InputDate.cs @@ -2,6 +2,7 @@ // 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.Diagnostics.CodeAnalysis; using System.Globalization; using Microsoft.AspNetCore.Components.Rendering; @@ -34,7 +35,7 @@ namespace Microsoft.AspNetCore.Components.Forms } /// - protected override string FormatValueAsString([AllowNull] TValue value) + protected override string FormatValueAsString(TValue? value) { switch (value) { @@ -48,7 +49,7 @@ namespace Microsoft.AspNetCore.Components.Forms } /// - protected override bool TryParseValueFromString(string? value, [MaybeNull] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage) + protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage) { // Unwrap nullable types. We don't have to deal with receiving empty values for nullable // types here, because the underlying InputBase already covers that. @@ -70,6 +71,7 @@ namespace Microsoft.AspNetCore.Components.Forms if (success) { + Debug.Assert(result != null); validationErrorMessage = null; return true; } @@ -80,7 +82,7 @@ namespace Microsoft.AspNetCore.Components.Forms } } - static bool TryParseDateTime(string? value, [MaybeNullWhen(false)] out TValue result) + private static bool TryParseDateTime(string? value, [MaybeNullWhen(false)] out TValue result) { var success = BindConverter.TryConvertToDateTime(value, CultureInfo.InvariantCulture, DateFormat, out var parsedValue); if (success) @@ -95,7 +97,7 @@ namespace Microsoft.AspNetCore.Components.Forms } } - static bool TryParseDateTimeOffset(string? value, [MaybeNullWhen(false)] out TValue result) + private static bool TryParseDateTimeOffset(string? value, [MaybeNullWhen(false)] out TValue result) { var success = BindConverter.TryConvertToDateTimeOffset(value, CultureInfo.InvariantCulture, DateFormat, out var parsedValue); if (success) diff --git a/src/Components/Web/src/Forms/InputExtensions.cs b/src/Components/Web/src/Forms/InputExtensions.cs index 748af5c78e..e0991b5cb3 100644 --- a/src/Components/Web/src/Forms/InputExtensions.cs +++ b/src/Components/Web/src/Forms/InputExtensions.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Components.Forms { internal static class InputExtensions { - public static bool TryParseSelectableValueFromString(this InputBase input, string? value, [MaybeNull] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage) + public static bool TryParseSelectableValueFromString(this InputBase input, string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage) { try { diff --git a/src/Components/Web/src/Forms/InputFile.cs b/src/Components/Web/src/Forms/InputFile.cs index ada1a96f10..7eb6546d4a 100644 --- a/src/Components/Web/src/Forms/InputFile.cs +++ b/src/Components/Web/src/Forms/InputFile.cs @@ -73,6 +73,11 @@ namespace Microsoft.AspNetCore.Components.Forms { var imageFile = await JSRuntime.InvokeAsync(InputFileInterop.ToImageFile, _inputFileElement, file.Id, format, maxWidth, maxHeight); + if (imageFile is null) + { + throw new InvalidOperationException("ToImageFile returned an unexpected null result."); + } + imageFile.Owner = this; return imageFile; diff --git a/src/Components/Web/src/Forms/InputFile/RemoteBrowserFileStream.cs b/src/Components/Web/src/Forms/InputFile/RemoteBrowserFileStream.cs index 8f712ad3de..2616ecef0c 100644 --- a/src/Components/Web/src/Forms/InputFile/RemoteBrowserFileStream.cs +++ b/src/Components/Web/src/Forms/InputFile/RemoteBrowserFileStream.cs @@ -4,6 +4,7 @@ using System; using System.Buffers; using System.IO.Pipelines; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.JSInterop; @@ -64,10 +65,10 @@ namespace Microsoft.AspNetCore.Components.Forms offset, segmentSize); - if (bytes.Length != segmentSize) + if (bytes is null || bytes.Length != segmentSize) { throw new InvalidOperationException( - $"A segment with size {bytes.Length} bytes was received, but {segmentSize} bytes were expected."); + $"A segment with size {bytes?.Length ?? 0} bytes was received, but {segmentSize} bytes were expected."); } bytes.CopyTo(pipeBuffer); diff --git a/src/Components/Web/src/Forms/InputNumber.cs b/src/Components/Web/src/Forms/InputNumber.cs index 51ac2c5241..7c51654a85 100644 --- a/src/Components/Web/src/Forms/InputNumber.cs +++ b/src/Components/Web/src/Forms/InputNumber.cs @@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.Components.Forms } /// - protected override bool TryParseValueFromString(string? value, [MaybeNull] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage) + protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage) { if (BindConverter.TryConvertTo(value, CultureInfo.InvariantCulture, out result)) { @@ -74,7 +74,7 @@ namespace Microsoft.AspNetCore.Components.Forms /// /// The value to format. /// A string representation of the value. - protected override string? FormatValueAsString([AllowNull] TValue value) + protected override string? FormatValueAsString(TValue? value) { // Avoiding a cast to IFormattable to avoid boxing. switch (value) diff --git a/src/Components/Web/src/Forms/InputRadio.cs b/src/Components/Web/src/Forms/InputRadio.cs index 4a4ad46dc3..77df88376f 100644 --- a/src/Components/Web/src/Forms/InputRadio.cs +++ b/src/Components/Web/src/Forms/InputRadio.cs @@ -27,10 +27,8 @@ namespace Microsoft.AspNetCore.Components.Forms /// /// Gets or sets the value of this input. /// - [AllowNull] - [MaybeNull] [Parameter] - public TValue Value { get; set; } = default; + public TValue? Value { get; set; } /// /// Gets or sets the name of the parent input radio group. diff --git a/src/Components/Web/src/Forms/InputRadioGroup.cs b/src/Components/Web/src/Forms/InputRadioGroup.cs index 661bd91e24..8d5e663a42 100644 --- a/src/Components/Web/src/Forms/InputRadioGroup.cs +++ b/src/Components/Web/src/Forms/InputRadioGroup.cs @@ -52,7 +52,7 @@ namespace Microsoft.AspNetCore.Components.Forms } /// - protected override bool TryParseValueFromString(string? value, [MaybeNull] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage) + protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage) => this.TryParseSelectableValueFromString(value, out result, out validationErrorMessage); } } diff --git a/src/Components/Web/src/Forms/InputSelect.cs b/src/Components/Web/src/Forms/InputSelect.cs index b7d5dd7025..1a77ff8372 100644 --- a/src/Components/Web/src/Forms/InputSelect.cs +++ b/src/Components/Web/src/Forms/InputSelect.cs @@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Components.Forms } /// - protected override bool TryParseValueFromString(string? value, [MaybeNull] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage) + protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage) => this.TryParseSelectableValueFromString(value, out result, out validationErrorMessage); } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/IJSInProcessRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/IJSInProcessRuntime.cs index d1ccd639db..37b8f37f3a 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/IJSInProcessRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/IJSInProcessRuntime.cs @@ -11,10 +11,10 @@ namespace Microsoft.JSInterop /// /// Invokes the specified JavaScript function synchronously. /// - /// The JSON-serializable return type. + /// The JSON-serializable return type. /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. /// JSON-serializable arguments. - /// An instance of obtained by JSON-deserializing the return value. - T Invoke(string identifier, params object?[]? args); + /// An instance of obtained by JSON-deserializing the return value. + TResult Invoke(string identifier, params object?[]? args); } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSInProcessObjectReference.cs b/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSInProcessObjectReference.cs index 6867c98c78..1f48b8c896 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSInProcessObjectReference.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSInProcessObjectReference.cs @@ -23,7 +23,6 @@ namespace Microsoft.JSInterop.Implementation } /// - [return: MaybeNull] public TValue Invoke(string identifier, params object?[]? args) { ThrowIfDisposed(); diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntime.cs index 44b35f6d3f..e2263333f2 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntime.cs @@ -23,7 +23,6 @@ namespace Microsoft.JSInterop id => new JSInProcessObjectReference(this, id))); } - [return: MaybeNull] internal TValue Invoke(string identifier, long targetInstanceId, params object?[]? args) { var resultJson = InvokeJS( @@ -32,12 +31,15 @@ namespace Microsoft.JSInterop JSCallResultTypeHelper.FromGeneric(), targetInstanceId); + // While the result of deserialization could be null, we're making a + // quality of life decision and letting users explicitly determine if they expect + // null by specifying TValue? as the expected return type. if (resultJson is null) { - return default; + return default!; } - return JsonSerializer.Deserialize(resultJson, JsonSerializerOptions); + return JsonSerializer.Deserialize(resultJson, JsonSerializerOptions)!; } /// @@ -47,7 +49,6 @@ namespace Microsoft.JSInterop /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. /// JSON-serializable arguments. /// An instance of obtained by JSON-deserializing the return value. - [return: MaybeNull] public TValue Invoke(string identifier, params object?[]? args) => Invoke(identifier, 0, args); diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs index 7d37460e55..d6f95fe126 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs @@ -102,7 +102,7 @@ namespace Microsoft.JSInterop { var taskId = Interlocked.Increment(ref _nextPendingTaskId); var tcs = new TaskCompletionSource(); - if (cancellationToken != default) + if (cancellationToken.CanBeCanceled) { _cancellationRegistrations[taskId] = cancellationToken.Register(() => { diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs index 354eddbbf6..68ab076a66 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs @@ -41,7 +41,7 @@ namespace Microsoft.JSInterop /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. /// JSON-serializable arguments. /// An instance of obtained by JSON-deserializing the return value. - public static ValueTask InvokeAsync(this IJSRuntime jsRuntime, string identifier, params object[] args) + public static ValueTask InvokeAsync(this IJSRuntime jsRuntime, string identifier, params object?[]? args) { if (jsRuntime is null) { @@ -63,7 +63,7 @@ namespace Microsoft.JSInterop /// /// JSON-serializable arguments. /// An instance of obtained by JSON-deserializing the return value. - public static ValueTask InvokeAsync(this IJSRuntime jsRuntime, string identifier, CancellationToken cancellationToken, params object[] args) + public static ValueTask InvokeAsync(this IJSRuntime jsRuntime, string identifier, CancellationToken cancellationToken, params object?[]? args) { if (jsRuntime is null) { @@ -102,7 +102,7 @@ namespace Microsoft.JSInterop /// The duration after which to cancel the async operation. Overrides default timeouts (). /// JSON-serializable arguments. /// A that represents the asynchronous invocation operation. - public static async ValueTask InvokeAsync(this IJSRuntime jsRuntime, string identifier, TimeSpan timeout, params object[] args) + public static async ValueTask InvokeAsync(this IJSRuntime jsRuntime, string identifier, TimeSpan timeout, params object?[]? args) { if (jsRuntime is null) { diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs index 543b872ab7..86de6ca371 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; using System.Text.Json; @@ -145,6 +146,7 @@ namespace Microsoft.JSInterop ref reader); Assert.True(task.IsCompleted); var poco = task.Result; + Debug.Assert(poco != null); Assert.Equal(10, poco.Id); Assert.Equal("Test", poco.Name); } @@ -167,6 +169,7 @@ namespace Microsoft.JSInterop ref reader); Assert.True(task.IsCompleted); var poco = task.Result; + Debug.Assert(poco != null); Assert.Equal(10, poco.Id); Assert.Equal("Test", poco.Name); }