From d8a66bcc6f8f659a7a58121af381b59ac148c34a Mon Sep 17 00:00:00 2001 From: rolandVi Date: Thu, 10 Jul 2025 15:04:26 +0200 Subject: [PATCH] Enable IJSObjectReference to handle null/undefined values --- .../src/src/Microsoft.JSInterop.ts | 38 +++++++++++-------- .../src/test/CallDispatcher.test.ts | 23 ++++++++++- .../JSObjectReferenceJsonConverter.cs | 6 +++ .../JSObjectReferenceJsonConverterTest.cs | 13 +++++++ 4 files changed, 63 insertions(+), 17 deletions(-) diff --git a/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts b/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts index fdd7a4ed65ec..f674d5c688e2 100644 --- a/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts +++ b/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts @@ -155,6 +155,12 @@ export module DotNet { * @throws Error if the given value is not an Object. */ export function createJSObjectReference(jsObject: any): any { + if (jsObject === null || jsObject === undefined) { + return { + [jsObjectIdKey]: -1 + }; + } + if (jsObject && (typeof jsObject === "object" || jsObject instanceof Function)) { cachedJSObjectsById[nextJsObjectId] = new JSObject(jsObject); @@ -220,7 +226,7 @@ export module DotNet { export function disposeJSObjectReference(jsObjectReference: any): void { const id = jsObjectReference && jsObjectReference[jsObjectIdKey]; - if (typeof id === "number") { + if (typeof id === "number" && id !== -1) { disposeJSObjectReferenceById(id); } } @@ -573,7 +579,7 @@ export module DotNet { } /** Traverses the object hierarchy to find an object member specified by the identifier. - * + * * @param obj Root object to search in. * @param identifier Complete identifier of the member to find, e.g. "document.location.href". * @returns A tuple containing the immediate parent of the member and the member name. @@ -586,19 +592,19 @@ export module DotNet { // Error handling in case of undefined last key depends on the type of operation. for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; - + if (current && typeof current === 'object' && key in current) { current = current[key]; } else { throw new Error(`Could not find '${identifier}' ('${key}' was undefined).`); } } - + return [current, keys[keys.length - 1]]; } /** Takes an object member and a call type and returns a function that performs the operation specified by the call type on the member. - * + * * @param parent Immediate parent of the accessed object member. * @param memberName Name (key) of the accessed member. * @param callType The type of the operation to perform on the member. @@ -640,50 +646,50 @@ export module DotNet { if (!(propName in obj)) { return false; } - + // If the property is present we examine its descriptor, potentially needing to walk up the prototype chain. while (obj !== undefined) { const descriptor = Object.getOwnPropertyDescriptor(obj, propName); - + if (descriptor) { // Return true for data property if (descriptor.hasOwnProperty('value')) { return true } - + // Return true for accessor property with defined getter. return descriptor.hasOwnProperty('get') && typeof descriptor.get === 'function'; } - + obj = Object.getPrototypeOf(obj); } - + return false; } - + function isWritableProperty(obj: any, propName: string) { // Return true for missing property if the property can be added. if (!(propName in obj)) { return Object.isExtensible(obj); } - + // If the property is present we examine its descriptor, potentially needing to walk up the prototype chain. while (obj !== undefined) { const descriptor = Object.getOwnPropertyDescriptor(obj, propName); - + if (descriptor) { // Return true for writable data property. if (descriptor.hasOwnProperty('value') && descriptor.writable) { return true; } - + // Return true for accessor property with defined setter. return descriptor.hasOwnProperty('set') && typeof descriptor.set === 'function'; } - + obj = Object.getPrototypeOf(obj); } - + return false; } diff --git a/src/JSInterop/Microsoft.JSInterop.JS/src/test/CallDispatcher.test.ts b/src/JSInterop/Microsoft.JSInterop.JS/src/test/CallDispatcher.test.ts index 38ee0cc07996..65a8c41efed2 100644 --- a/src/JSInterop/Microsoft.JSInterop.JS/src/test/CallDispatcher.test.ts +++ b/src/JSInterop/Microsoft.JSInterop.JS/src/test/CallDispatcher.test.ts @@ -395,4 +395,25 @@ describe("CallDispatcher", () => { expect(result2).toBe("30"); }); -}); \ No newline at end of file + + test("createJSObjectReference: Handles null values without throwing", () => { + const nullRef = DotNet.createJSObjectReference(null); + expect(nullRef).toEqual({ [jsObjectId]: -1 }); + }); + + test("createJSObjectReference: Handles undefined values without throwing", () => { + const undefinedRef = DotNet.createJSObjectReference(undefined); + expect(undefinedRef).toEqual({ [jsObjectId]: -1 }); + }); + + test("disposeJSObjectReference: Safely handles null reference disposal", () => { + const nullRef = DotNet.createJSObjectReference(null); + expect(() => DotNet.disposeJSObjectReference(nullRef)).not.toThrow(); + }); + + test("createJSObjectReference: Still throws for invalid types", () => { + expect(() => DotNet.createJSObjectReference("string")).toThrow(); + expect(() => DotNet.createJSObjectReference(123)).toThrow(); + expect(() => DotNet.createJSObjectReference(true)).toThrow(); + }); +}); diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSObjectReferenceJsonConverter.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSObjectReferenceJsonConverter.cs index 563291822a4a..e4bf9db0c409 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSObjectReferenceJsonConverter.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSObjectReferenceJsonConverter.cs @@ -22,6 +22,12 @@ public override bool CanConvert(Type typeToConvert) public override IJSObjectReference? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var id = JSObjectReferenceJsonWorker.ReadJSObjectReferenceIdentifier(ref reader); + + if (id == -1) + { + return null; + } + return new JSObjectReference(_jsRuntime, id); } diff --git a/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/JSObjectReferenceJsonConverterTest.cs b/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/JSObjectReferenceJsonConverterTest.cs index 63b5a889782a..383c06819317 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/JSObjectReferenceJsonConverterTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/JSObjectReferenceJsonConverterTest.cs @@ -87,4 +87,17 @@ public void Write_WritesValidJson() // Assert Assert.Equal($"{{\"__jsObjectId\":{jsObjectRef.Id}}}", json); } + + [Fact] + public void Read_ReturnsNull_WhenJSObjectIdIsMinusOne() + { + // Arrange + var json = "{\"__jsObjectId\":-1}"; + + // Act + var deserialized = JsonSerializer.Deserialize(json, JsonSerializerOptions); + + // Assert + Assert.Null(deserialized); + } }