From 8b02460038eebfff073a511d5133cd347614a500 Mon Sep 17 00:00:00 2001 From: Regenhardt Marlon Date: Tue, 19 Dec 2023 18:09:14 +0100 Subject: [PATCH 1/4] JSInterop: Enable returning null for a nullable value type When a result value is null, and the return type is a nullable value type, Convert.ChangeType throws instead of returning the null value. #30366 --- .../src/Infrastructure/TaskGenericsUtil.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/TaskGenericsUtil.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/TaskGenericsUtil.cs index a03d87536db1..ee99390a5d89 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/TaskGenericsUtil.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/TaskGenericsUtil.cs @@ -89,7 +89,9 @@ public void SetResult(object tcs, object? result) // If necessary, attempt a cast var typedResult = result is T resultT ? resultT - : (T)Convert.ChangeType(result, typeof(T), CultureInfo.InvariantCulture)!; + : result == null && default(T) == null + ? default(T) + : (T)Convert.ChangeType(result, typeof(T), CultureInfo.InvariantCulture)!; typedTcs.SetResult(typedResult!); } From 0a868642abd7ccbd7f828d7d1c382d245ef064ec Mon Sep 17 00:00:00 2001 From: Marlon Regenhardt Date: Mon, 10 Mar 2025 17:06:08 +0100 Subject: [PATCH 2/4] Test JSRuntime for value type handling --- .../E2ETest/Tests/InteropValueTypesTest.cs | 62 +++++++++++++++++ .../Tests/StartupErrorNotificationTest.cs | 2 +- .../test/testassets/BasicTestApp/Index.razor | 1 + .../InteropValueTypesComponent.razor | 69 +++++++++++++++++++ 4 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 src/Components/test/E2ETest/Tests/InteropValueTypesTest.cs create mode 100644 src/Components/test/testassets/BasicTestApp/InteropValueTypesComponent.razor diff --git a/src/Components/test/E2ETest/Tests/InteropValueTypesTest.cs b/src/Components/test/E2ETest/Tests/InteropValueTypesTest.cs new file mode 100644 index 000000000000..53ad9387744b --- /dev/null +++ b/src/Components/test/E2ETest/Tests/InteropValueTypesTest.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using BasicTestApp; +using Microsoft.AspNetCore.Components.E2ETest; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.AspNetCore.E2ETesting; +using OpenQA.Selenium; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Components.E2ETests.Tests; + +public class InteropValueTypesTest( + BrowserFixture browserFixture, + ToggleExecutionModeServerFixture serverFixture, + ITestOutputHelper output) + : ServerTestBase>(browserFixture, serverFixture, output) +{ + protected override void InitializeAsyncCore() + { + Navigate(ServerPathBase); + Browser.MountTestComponent(); + + var interopButton = Browser.Exists(By.Id("btn-interop")); + interopButton.Click(); + Browser.Exists(By.Id("done-with-interop")); + } + + [Fact] + public void CanRetrieveStoredGuidAsString() + { + var stringGuidElement = Browser.Exists(By.Id("string-get-by-interop")); + + Browser.NotEqual(string.Empty, () => stringGuidElement.Text); + Browser.NotEqual(default, () => Guid.Parse(stringGuidElement.Text)); + } + + [Fact] + public void CanRetrieveStoredGuidAsGuid() + { + var guidElement = Browser.Exists(By.Id("guid-get-by-interop")); + + Browser.NotEqual(default, () => Guid.Parse(guidElement.Text)); + } + + [Fact] + public void CanRetrieveStoredGuidAsNullableGuid() + { + var nullableGuidElement = Browser.Exists(By.Id("nullable-guid-get-by-interop")); + + Browser.NotEqual(default, () => Guid.Parse(nullableGuidElement.Text)); + } + + [Fact] + public void CanRetrieveNullAsNullableGuid() + { + var nullableGuidElement = Browser.Exists(By.Id("null-loaded-into-nullable")); + + Browser.Equal(true.ToString(), () => nullableGuidElement.Text); + } +} diff --git a/src/Components/test/E2ETest/Tests/StartupErrorNotificationTest.cs b/src/Components/test/E2ETest/Tests/StartupErrorNotificationTest.cs index 37c70e381e29..b8d69471694e 100644 --- a/src/Components/test/E2ETest/Tests/StartupErrorNotificationTest.cs +++ b/src/Components/test/E2ETest/Tests/StartupErrorNotificationTest.cs @@ -27,7 +27,7 @@ public StartupErrorNotificationTest( public void DisplaysNotificationForStartupException(bool errorIsAsync) { var url = $"{ServerPathBase}?error={(errorIsAsync ? "async" : "sync")}"; - + Navigate(url); var errorUiElem = Browser.Exists(By.Id("blazor-error-ui"), TimeSpan.FromSeconds(10)); Assert.NotNull(errorUiElem); diff --git a/src/Components/test/testassets/BasicTestApp/Index.razor b/src/Components/test/testassets/BasicTestApp/Index.razor index 3ce2eb27dc95..8b077bab3be8 100644 --- a/src/Components/test/testassets/BasicTestApp/Index.razor +++ b/src/Components/test/testassets/BasicTestApp/Index.razor @@ -66,6 +66,7 @@ + diff --git a/src/Components/test/testassets/BasicTestApp/InteropValueTypesComponent.razor b/src/Components/test/testassets/BasicTestApp/InteropValueTypesComponent.razor new file mode 100644 index 000000000000..21172b4dac4a --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/InteropValueTypesComponent.razor @@ -0,0 +1,69 @@ +@using Microsoft.JSInterop +@inject IJSRuntime JSRuntime + + + +

+ This component shows it's possible to save and load value types (in this case, a guid) + to and from localstorage, + and also that a null value can be loaded into a nullable value type, i.e. a Guid?. +

+ +

+ String value from guid: + @(StringFromInterop ?? "No value yet") +

+ +

+ Guid value from guid is default(Guid) if it wasn't loaded correctly: + @GuidFromInterop +

+ +

+ Nullable Guid value from guid is null if it wasn't loaded correctly: + @NullableGuidFromInterop +

+ +

+ Nullable load result is false if the null value wasn't loaded into the nullable guid correctly: + @NullableLoadResult +

+ +@if (DoneWithInterop) +{ +

+ Done with interop! +

+} + +@code { + public Guid Guid { get; } = Guid.NewGuid(); + public string StringFromInterop { get; set; } + public Guid GuidFromInterop { get; set; } + public Guid? NullableGuidFromInterop { get; set; } + public bool NullableLoadResult { get; set; } + public bool DoneWithInterop { get; set; } + + private async Task InvokeInteropAsync() + { + // Store guid in localStorage + await JSRuntime.InvokeVoidAsync("localStorage.setItem", "guid", Guid); + + // Retrieve guid from localStorage as string because it's a simple conversion + StringFromInterop = await JSRuntime.InvokeAsync("localStorage.getItem", "guid"); + + // Retrieve guid from localStorage as Guid + GuidFromInterop = await JSRuntime.InvokeAsync("localStorage.getItem", "guid"); + + // Retrieve guid from localStorage as Guid? + NullableGuidFromInterop = await JSRuntime.InvokeAsync("localStorage.getItem", "guid"); + + // Retrieve null from localStorage as Guid? + var nullableGuid = await JSRuntime.InvokeAsync("localStorage.getItem", "nothingHere"); + + // If the retrieval crashes, the bool stays false + NullableLoadResult = !nullableGuid.HasValue; + + DoneWithInterop = true; + } +} From 8e9115a5361e868558300fb940f88f51dee1882b Mon Sep 17 00:00:00 2001 From: Marlon Regenhardt Date: Fri, 21 Mar 2025 08:40:37 +0100 Subject: [PATCH 3/4] Add inline docs --- .../Microsoft.JSInterop/src/Infrastructure/TaskGenericsUtil.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/TaskGenericsUtil.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/TaskGenericsUtil.cs index ee99390a5d89..d35504aea6d0 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/TaskGenericsUtil.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/TaskGenericsUtil.cs @@ -89,7 +89,7 @@ public void SetResult(object tcs, object? result) // If necessary, attempt a cast var typedResult = result is T resultT ? resultT - : result == null && default(T) == null + : result == null && default(T) == null // ChangeType can't convert null to value types ? default(T) : (T)Convert.ChangeType(result, typeof(T), CultureInfo.InvariantCulture)!; From 86a68f36a9409859987306ec86b5a3345eeb31f7 Mon Sep 17 00:00:00 2001 From: Marlon Regenhardt Date: Thu, 10 Apr 2025 13:34:38 +0200 Subject: [PATCH 4/4] Explicitly check for nullable type instead of default value --- .../Microsoft.JSInterop/src/Infrastructure/TaskGenericsUtil.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/TaskGenericsUtil.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/TaskGenericsUtil.cs index d35504aea6d0..d18eecab82e6 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/TaskGenericsUtil.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/TaskGenericsUtil.cs @@ -89,7 +89,7 @@ public void SetResult(object tcs, object? result) // If necessary, attempt a cast var typedResult = result is T resultT ? resultT - : result == null && default(T) == null // ChangeType can't convert null to value types + : result == null && typeof(T).IsGenericType && typeof(T).GetGenericTypeDefinition() == typeof(Nullable<>) // ChangeType can't convert null to value types ? default(T) : (T)Convert.ChangeType(result, typeof(T), CultureInfo.InvariantCulture)!;