Skip to content

Replace use of InvokeNew with InvokeConstructor in Blazor JS interop #62588

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ internal sealed class UnsupportedJavaScriptRuntime : IJSRuntime
ValueTask<TValue> IJSRuntime.InvokeAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier, object?[]? args)
=> throw new InvalidOperationException(Message);

public ValueTask<IJSObjectReference> InvokeNewAsync(string identifier, object?[]? args)
public ValueTask<IJSObjectReference> InvokeConstructorAsync(string identifier, object?[]? args)
=> throw new InvalidOperationException(Message);

public ValueTask<IJSObjectReference> InvokeNewAsync(string identifier, CancellationToken cancellationToken, object?[]? args)
public ValueTask<IJSObjectReference> InvokeConstructorAsync(string identifier, CancellationToken cancellationToken, object?[]? args)
=> throw new InvalidOperationException(Message);

public ValueTask<TValue> GetValueAsync<[DynamicallyAccessedMembers(JsonSerialized)] TValue>(string identifier)
Expand Down
8 changes: 4 additions & 4 deletions src/Components/Server/test/ProtectedBrowserStorageTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -365,15 +365,15 @@ public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToke
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args)
=> InvokeAsync<TValue>(identifier, cancellationToken: CancellationToken.None, args: args);

public ValueTask<IJSObjectReference> InvokeNewAsync(string identifier, object[] args)
public ValueTask<IJSObjectReference> InvokeConstructorAsync(string identifier, object[] args)
{
Invocations.Add((identifier, args, JSCallType.NewCall));
Invocations.Add((identifier, args, JSCallType.ConstructorCall));
return (ValueTask<IJSObjectReference>)NextInvocationResult;
}

public ValueTask<IJSObjectReference> InvokeNewAsync(string identifier, CancellationToken cancellationToken, object[] args)
public ValueTask<IJSObjectReference> InvokeConstructorAsync(string identifier, CancellationToken cancellationToken, object[] args)
{
Invocations.Add((identifier, args, JSCallType.NewCall));
Invocations.Add((identifier, args, JSCallType.ConstructorCall));
return (ValueTask<IJSObjectReference>)NextInvocationResult;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -549,10 +549,10 @@ public ValueTask<TValue> GetValueAsync<TValue>(string identifier)
public ValueTask<TValue> GetValueAsync<TValue>(string identifier, CancellationToken cancellationToken)
=> throw new NotImplementedException();

public ValueTask<IJSObjectReference> InvokeNewAsync(string identifier, object[] args)
public ValueTask<IJSObjectReference> InvokeConstructorAsync(string identifier, object[] args)
=> throw new NotImplementedException();

public ValueTask<IJSObjectReference> InvokeNewAsync(string identifier, CancellationToken cancellationToken, object[] args)
public ValueTask<IJSObjectReference> InvokeConstructorAsync(string identifier, CancellationToken cancellationToken, object[] args)
=> throw new NotImplementedException();

public ValueTask SetValueAsync<TValue>(string identifier, TValue value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -790,10 +790,10 @@ public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToke
return default;
}

public ValueTask<IJSObjectReference> InvokeNewAsync(string identifier, object[] args)
public ValueTask<IJSObjectReference> InvokeConstructorAsync(string identifier, object[] args)
=> throw new NotImplementedException();

public ValueTask<IJSObjectReference> InvokeNewAsync(string identifier, CancellationToken cancellationToken, object[] args)
public ValueTask<IJSObjectReference> InvokeConstructorAsync(string identifier, CancellationToken cancellationToken, object[] args)
=> throw new NotImplementedException();

public ValueTask<TValue> GetValueAsync<TValue>(string identifier)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,10 @@ public virtual ValueTask<TValue> InvokeAsync<TValue>(string identifier, Cancella
public async ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args)
=> await InvokeAsync<TValue>(identifier, CancellationToken.None, args);

public ValueTask<IJSObjectReference> InvokeNewAsync(string identifier, object[] args)
public ValueTask<IJSObjectReference> InvokeConstructorAsync(string identifier, object[] args)
=> throw new NotImplementedException();

public ValueTask<IJSObjectReference> InvokeNewAsync(string identifier, CancellationToken cancellationToken, object[] args)
public ValueTask<IJSObjectReference> InvokeConstructorAsync(string identifier, CancellationToken cancellationToken, object[] args)
=> throw new NotImplementedException();

public ValueTask<TValue> GetValueAsync<TValue>(string identifier)
Expand Down
20 changes: 10 additions & 10 deletions src/Components/test/E2ETest/Tests/InteropTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,11 @@ public void CanInvokeInteropMethods()
["setValueToSetterAsync"] = "40",
["setValueToUndefinedPropertyAsync"] = "50",
["setValueToGetterAsync"] = "Success",
// InvokeNew tests
["invokeNewWithClassConstructorAsync"] = "Success",
["invokeNewWithClassConstructorAsync.dataProperty"] = "abraka",
["invokeNewWithClassConstructorAsync.function"] = "6",
["invokeNewWithNonConstructorAsync"] = "Success",
// InvokeConstructor tests
["invokeConstructorWithClassConstructorAsync"] = "Success",
["invokeConstructorWithClassConstructorAsync.dataProperty"] = "abraka",
["invokeConstructorWithClassConstructorAsync.function"] = "6",
["invokeConstructorWithNonConstructorAsync"] = "Success",
// Function reference tests
["changeFunctionViaObjectReferenceAsync"] = "42"
};
Expand Down Expand Up @@ -168,11 +168,11 @@ public void CanInvokeInteropMethods()
["setValueToSetter"] = "40",
["setValueToUndefinedProperty"] = "50",
["setValueToGetter"] = "Success",
// InvokeNew tests
["invokeNewWithClassConstructor"] = "Success",
["invokeNewWithClassConstructor.dataProperty"] = "abraka",
["invokeNewWithClassConstructor.function"] = "6",
["invokeNewWithNonConstructor"] = "Success",
// InvokeConstructor tests
["invokeConstructorWithClassConstructor"] = "Success",
["invokeConstructorWithClassConstructor.dataProperty"] = "abraka",
["invokeConstructorWithClassConstructor.function"] = "6",
["invokeConstructorWithNonConstructor"] = "Success",
// Function reference tests
["changeFunctionViaObjectReference"] = "42"
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@
</div>

<div style="margin-top: 2em">
<button @onclick="CreateInstanceByConstructorFunction">Call constructor function InvokeNewAsync</button>
<button @onclick="CreateInstanceByClassConstructor">Call class constructor with InvokeNewAsync</button>
<button @onclick="CreateInstanceByConstructorFunction">Call constructor function InvokeConstructorAsync</button>
<button @onclick="CreateInstanceByClassConstructor">Call class constructor with InvokeConstructorAsync</button>
<button @onclick="ChangeInstanceMethodWithFunctionReference">Change instance method with function reference</button>
<span>@InstanceMessage</span>
</div>
Expand Down Expand Up @@ -147,13 +147,13 @@

private async Task CreateInstanceByConstructorFunction()
{
var dogRef = await JSRuntime.InvokeNewAsync("Dog", "A dog");
var dogRef = await JSRuntime.InvokeConstructorAsync("Dog", "A dog");
InstanceMessage = await dogRef.InvokeAsync<string>("bark");
}

private async Task CreateInstanceByClassConstructor()
{
var catRef = await JSRuntime.InvokeNewAsync("Cat", "A cat");
var catRef = await JSRuntime.InvokeConstructorAsync("Cat", "A cat");
InstanceMessage = await catRef.InvokeAsync<string>("meow");
}

Expand All @@ -169,9 +169,9 @@

private async Task ChangeInstanceMethodWithFunctionReference()
{
var dogRef = await JSRuntime.InvokeNewAsync("Dog", "A dog");
var dogRef = await JSRuntime.InvokeConstructorAsync("Dog", "A dog");
var dogFuncRef = await dogRef.GetValueAsync<IJSObjectReference>("bark");
var catRef = await JSRuntime.InvokeNewAsync("Cat", "A cat");
var catRef = await JSRuntime.InvokeConstructorAsync("Cat", "A cat");
await catRef.SetValueAsync("meow", dogFuncRef);
InstanceMessage = await catRef.InvokeAsync<string>("meow");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,11 +294,11 @@
SetValueTests();
}

await InvokeNewAsyncTests();
await InvokeConstructorAsyncTests();

if (shouldSupportSyncInterop)
{
InvokeNewTests();
InvokeConstructorTests();
}

await FunctionReferenceAsyncTests();
Expand Down Expand Up @@ -560,58 +560,58 @@
}
}

private async Task InvokeNewAsyncTests()
private async Task InvokeConstructorAsyncTests()
{
var testClassRef = await JSRuntime.InvokeNewAsync("jsInteropTests.TestClass", "abraka");
var testClassRef = await JSRuntime.InvokeConstructorAsync("jsInteropTests.TestClass", "abraka");

ReturnValues["invokeNewWithClassConstructorAsync"] = testClassRef is IJSObjectReference ? "Success" : "Failure";
ReturnValues["invokeNewWithClassConstructorAsync.dataProperty"] = await testClassRef.GetValueAsync<string>("text");
ReturnValues["invokeNewWithClassConstructorAsync.function"] = (await testClassRef.InvokeAsync<int>("getTextLength")).ToString();
ReturnValues["invokeConstructorWithClassConstructorAsync"] = testClassRef is IJSObjectReference ? "Success" : "Failure";
ReturnValues["invokeConstructorWithClassConstructorAsync.dataProperty"] = await testClassRef.GetValueAsync<string>("text");
ReturnValues["invokeConstructorWithClassConstructorAsync.function"] = (await testClassRef.InvokeAsync<int>("getTextLength")).ToString();

try
{
var nonConstructorRef = await JSRuntime.InvokeNewAsync("jsInteropTests.nonConstructorFunction");
ReturnValues["invokeNewWithNonConstructorAsync"] = nonConstructorRef is null ? "Failure: null" : "Failure: not null";
var nonConstructorRef = await JSRuntime.InvokeConstructorAsync("jsInteropTests.nonConstructorFunction");
ReturnValues["invokeConstructorWithNonConstructorAsync"] = nonConstructorRef is null ? "Failure: null" : "Failure: not null";
}
catch (JSException)
{
ReturnValues["invokeNewWithNonConstructorAsync"] = "Success";
ReturnValues["invokeConstructorWithNonConstructorAsync"] = "Success";
}
catch (Exception ex)
{
ReturnValues["invokeNewWithNonConstructorAsync"] = $"Failure: {ex.Message}";
ReturnValues["invokeConstructorWithNonConstructorAsync"] = $"Failure: {ex.Message}";
}
}

private void InvokeNewTests()
private void InvokeConstructorTests()
{
var inProcRuntime = ((IJSInProcessRuntime)JSRuntime);

var testClassRef = inProcRuntime.InvokeNew("jsInteropTests.TestClass", "abraka");
var testClassRef = inProcRuntime.InvokeConstructor("jsInteropTests.TestClass", "abraka");

ReturnValues["invokeNewWithClassConstructor"] = testClassRef is IJSInProcessObjectReference ? "Success" : "Failure";
ReturnValues["invokeNewWithClassConstructor.dataProperty"] = testClassRef.GetValue<string>("text");
ReturnValues["invokeNewWithClassConstructor.function"] = testClassRef.Invoke<int>("getTextLength").ToString();
ReturnValues["invokeConstructorWithClassConstructor"] = testClassRef is IJSInProcessObjectReference ? "Success" : "Failure";
ReturnValues["invokeConstructorWithClassConstructor.dataProperty"] = testClassRef.GetValue<string>("text");
ReturnValues["invokeConstructorWithClassConstructor.function"] = testClassRef.Invoke<int>("getTextLength").ToString();

try
{
var nonConstructorRef = inProcRuntime.InvokeNew("jsInteropTests.nonConstructorFunction");
ReturnValues["invokeNewWithNonConstructor"] = nonConstructorRef is null ? "Failure: null" : "Failure: not null";
var nonConstructorRef = inProcRuntime.InvokeConstructor("jsInteropTests.nonConstructorFunction");
ReturnValues["invokeConstructorWithNonConstructor"] = nonConstructorRef is null ? "Failure: null" : "Failure: not null";
}
catch (JSException)
{
ReturnValues["invokeNewWithNonConstructor"] = "Success";
ReturnValues["invokeConstructorWithNonConstructor"] = "Success";
}
catch (Exception ex)
{
ReturnValues["invokeNewWithNonConstructor"] = $"Failure: {ex.Message}";
ReturnValues["invokeConstructorWithNonConstructor"] = $"Failure: {ex.Message}";
}
}

private async Task FunctionReferenceAsyncTests()
{
var funcRef = await JSRuntime.GetValueAsync<IJSObjectReference>("jsInteropTests.nonConstructorFunction");
var testClassRef = await JSRuntime.InvokeNewAsync("jsInteropTests.TestClass", "abraka");
var testClassRef = await JSRuntime.InvokeConstructorAsync("jsInteropTests.TestClass", "abraka");
await testClassRef.SetValueAsync("getTextLength", funcRef);
ReturnValues["changeFunctionViaObjectReferenceAsync"] = (await testClassRef.InvokeAsync<int>("getTextLength")).ToString();
}
Expand All @@ -621,7 +621,7 @@
var inProcRuntime = ((IJSInProcessRuntime)JSRuntime);

var funcRef = inProcRuntime.GetValue<IJSObjectReference>("jsInteropTests.nonConstructorFunction");
var testClassRef = inProcRuntime.InvokeNew("jsInteropTests.TestClass", "abraka");
var testClassRef = inProcRuntime.InvokeConstructor("jsInteropTests.TestClass", "abraka");
testClassRef.SetValue("getTextLength", funcRef);
ReturnValues["changeFunctionViaObjectReference"] = testClassRef.Invoke<int>("getTextLength").ToString();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export module DotNet {
*/
export enum JSCallType {
FunctionCall = 1,
NewCall = 2,
ConstructorCall = 2,
GetValue = 3,
SetValue = 4
}
Expand Down Expand Up @@ -573,7 +573,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.
Expand All @@ -586,19 +586,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.
Expand All @@ -614,7 +614,7 @@ export module DotNet {
} else {
throw new Error(`The value '${identifier}' is not a function.`);
}
case JSCallType.NewCall:
case JSCallType.ConstructorCall:
const ctor = parent[memberName];
if (ctor instanceof Function) {
const bound = ctor.bind(parent);
Expand All @@ -640,50 +640,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;
}

Expand Down
Loading
Loading