-
Notifications
You must be signed in to change notification settings - Fork 10.1k
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
Bypasses deserialization of return value when 'InvokeVoidAsync' is used. #22056
Bypasses deserialization of return value when 'InvokeVoidAsync' is used. #22056
Conversation
{ | ||
var result = JsonSerializer.Deserialize(ref jsonReader, resultType, JsonSerializerOptions); | ||
TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, result); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Curious, is there a downside to leaving tcs
hanging here if resultType
is VoidReturn
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, yes we definitely do not want to leave tcs
hanging. This would prevent the caller from knowing when their call had completed.
Seems like we should be setting the result to null
since that's probably the best representation of the idea of "no value".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@watfordgnf - Nice catch! @SteveSandersonMS - Fixed.
/// <param name="args">JSON-serializable arguments.</param> | ||
public async ValueTask InvokeVoidAsync(string identifier, CancellationToken cancellationToken, object[] args) | ||
{ | ||
await this.InvokeAsync<VoidReturn>(identifier, cancellationToken, args); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of interest, is it possible to use System.Void
instead of VoidReturn
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could look at using reflection to invoke the method if you like... something like this:
public async ValueTask InvokeVoidAsync(string identifier, CancellationToken cancellationToken, object[] args)
{
var voidType = typeof(void);
var invokeAsyncMethod = this.GetType().GetMethod(nameof(InvokeAsync), new Type[] { typeof(string), typeof(CancellationToken), typeof(object[]) }).MakeGenericMethod(voidType);
var task = (Task)invokeAsyncMethod.Invoke(this, new object[] { identifier, cancellationToken, args });
await task;
}
This compiles but I haven't actually tested the code to see if it works. If you think it's worth pursuing I can look at getting my environment setup to test and/or create a unit test if you can point me in the right direction as to where those are. I've just been opening up the project directly since the JSInterop.slnf
file doesn't work and I'm not actually sure what solution uses this project.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather avoid reflection here if we can. Your previous solution was fine really!
@@ -26,7 +26,7 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string | |||
throw new ArgumentNullException(nameof(jsRuntime)); | |||
} | |||
|
|||
await jsRuntime.InvokeAsync<object>(identifier, args); | |||
await jsRuntime.InvokeVoidAsync(identifier, args); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These methods are currently calling themselves which results in infinite recursion.
I think this also needs to be exposed on the IJSRuntime
interface or use VoidReturn
here and make it internal / public.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doh! You're right. I thought about adding it to the interface as well but didn't since the current implementation just used extension methods. I think it makes sense to add it to the interface at this point... @SteveSandersonMS thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hearing no response I went ahead and added the methods to the interface. @SteveSandersonMS is there anything else you are looking for/need for this PR to proceed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for following up on this. I know this PR has been delayed for a long time due to slow reviews on our side, so I'd be keen to work with you and get this merged in the next few days or next week, depending on what works best for you.
Apart from my suggestion above about avoiding the breaking change, the main remaining thing would be adding a few suitable test cases into the test
project. Is that something you feel able to handle?
/// </param> | ||
/// <param name="args">JSON-serializable arguments.</param> | ||
/// <returns>A <see cref="ValueTask"/> that represents the asynchronous invocation operation.</returns> | ||
ValueTask InvokeVoidAsync(string identifier, CancellationToken cancellationToken, object[] args); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding these methods to IJSRuntime
would be a breaking change. If at all possible, we'd strive to avoid that.
Does it work to not add these, and instead only change the implementation of JSRuntimeExtensions.InvokeVoidAsync
? If you changed the implementation of that extension method to call jsRuntime.InvokeAsync<VoidReturn>
internally, then JSRuntime.cs
could still be aware of that magic return type and use it as a signal not to deserialize the JSON, and just be hardcoded to return default(VoidReturn)
which would be discarded by JSRuntimeExtensions.InvokeVoidAsync
.
Thinking more deeply about this, it occurs to me that rather than simply disregarding the returned JSON, we should really tell the JS-side code not to send any JSON response. Otherwise if the developer calls some function that returns a non-serializable object, then they will get an error even though they told us they didn't want a return value. It should be possible to tweak Are you open to a design change like that? I think this approach is necessary if the goal is really to fix the original issue #16632. The original issue described an error with serializing the JSON response, not with deserializing it. |
8767c09
to
ca19908
Compare
…okeAsync<VoidReturn>' for each 'InvokeVoidAsync' extension method.
/// <returns>A JSON representation of the result.</returns> | ||
protected abstract string? InvokeJS(string identifier, string? argsJson); | ||
protected abstract string? InvokeJS(string identifier, string? argsJson, bool? treatReturnAsVoid); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@SteveSandersonMS - It's not clear to me where this method is implemented? Not sure what needs to be updated to ensure that this new treatReturnAsVoid
parameter gets passed to the JavaScript side of things.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry... found it. I initially was just opening the JSInterop.csproj on it's own.... just found Components.slnf which contains everything... now if I can only get the solution to build :(. Some build tasks appear to be failing: Microsoft.AspNetCore.Razor.Tasks.RazorGenerate
... any thoughts? It looks like the some dlls in the artifacts
folder are not being created.
@SteveSandersonMS - Your last comments regarding needing to change this at the JS level make sense. I have updated the PR to hopefully reflect this line of thought. Re: tests - I can take a look at this and let you know if I need some guidance and/or someone more familiar with the project to implement. |
{ | ||
var noAsyncHandle = default(long); | ||
var result = InternalCalls.InvokeJSMarshalled(out var exception, ref noAsyncHandle, identifier, argsJson); | ||
var result = InternalCalls.InvokeJSMarshalled(out var exception, ref noAsyncHandle, identifier, argsJson, treatReturnAsVoid); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like from the comments in the InvokeJSMarshalled
method that there's some magic that happens here that passes this new boolean
arg to JS? Seems like it should just work? Or do we need to be passing the treatReturnAsVoid
parameter as part of the argsJson
and somehow extracting it on the JS side?
I recognize it's quite tricky to work out where all the pieces are here and how they all join together! I also realised when thinking about this more that we'll need to be careful about how this works to avoid breaking back-compatibility for people who already have custom implementations of To provide an example of how this could work, I've done a sketch implementation here: https://github.com/dotnet/aspnetcore/compare/stevesa/jsruntime-void-results?expand=1. My sketch implementation would only work if dotnet/runtime#39435 was merged first because it requires a change to the contract between .NET code and the Are you interested in taking this sketch and working it up into a finished implementation? It's very close to done, except:
|
TBH - This has moved beyond my initial endeavors for a "quick win" and I don't know if I have enough knowledge of the internal workings to take it across the finish line. |
Thanks for your contribution, @StevenRasmussen. |
When using the
InvokeVoidAsync
method, deserialization of the return object is not required. This bypasses the deserialization which currently causes an error to be thrown in this instance.Addresses #16632