-
Notifications
You must be signed in to change notification settings - Fork 825
In ObjectMethodExecutor, add FSharpAsync support #221
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
In ObjectMethodExecutor, add FSharpAsync support #221
Conversation
@SteveSandersonMS, |
{ | ||
var parameters = candidateMethodInfo.GetParameters(); | ||
if (parameters.Length == 3 | ||
&& TypesHaveSameIdentity(parameters[0].ParameterType, possibleFSharpAsyncGenericType) |
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.
Does anybody know why parameters[0].ParameterType == possibleFSharpAsyncGenericType
would be returning false
here, even when the two System.Type
instances really seem to refer to FSharpAsync<>
? Why are there two different System.Type
instances at runtime for what seems to be the same type?
As you can see, I've worked around this by comparing their Assembly
, Namespace
, and Name
properties, but ideally I'd like to understand why I can't just compare the Type
instances.
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.
Generic crazyness? What types are involved?
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.
Both parameters[0].ParameterType
and possibleFSharpAsyncGenericType
are instances of System.Type
, and both represent the same type, FSharpAsync<>
. However they are different System.Type
instances. On one of them, .FullName
is populated, and on the other, .FullName
is null
.
It's not really a problem - the workaround of comparing Assembly
/Namespace
/Name
works perfectly adequately - it's just not what I was expecting to happen.
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.
@davidfowl Can you think of someone specific on CoreFX we could ask this question? We don't need to block the PR on resolving it but I'm really curious why it wouldn't work...
@@ -255,14 +260,15 @@ private static MethodExecutor WrapVoidMethod(VoidMethodExecutor executor) | |||
|
|||
// var getAwaiterFunc = (object awaitable) => | |||
// (object)((CustomAwaitableType)awaitable).GetAwaiter(); | |||
var customAwaiterParam = Expression.Parameter(typeof(object), "awaitable"); | |||
var customAwaitableParam = Expression.Parameter(typeof(object), "awaitable"); |
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.
The variable rename here is just because the old name was wrong.
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.
Haven't fully finished reviewing, but some small initial comments :)
|
||
private static bool IsFSharpAsyncOpenGenericType(Type possibleFSharpAsyncGenericType) | ||
{ | ||
if (possibleFSharpAsyncGenericType?.FullName != "Microsoft.FSharp.Control.FSharpAsync`1") |
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.
Constant
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.
Also I think we (and certainly I) prefer string.Equals
with an explicit StringComparison.Ordinal
for clarity.
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 don't mind changing it to a const
, but out of interest, what's the reason? Obviously there are benefits if the literal value was repeated and might one day be changed, but it isn't repeated here, and practically can't ever change value.
I tend to think that moving the string contents away from the usage site makes it harder to reason about what the code does, because you have to scroll up and down to follow it.
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.
That's a reasonable point. And mostly our guidance is to do that in places where the literal will be used in multiple sites.
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.
OK, I've changed to string.Equals
in all the applicable places. I'll leave the nonrepeated constants inline for easier reading as per the discussion above.
{ | ||
var assembly = possibleFSharpAsyncGenericType.Assembly; | ||
var fsharpOptionType = assembly.GetType("Microsoft.FSharp.Core.FSharpOption`1"); | ||
var fsharpAsyncType = assembly.GetType("Microsoft.FSharp.Control.FSharpAsync"); |
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.
More Constants :)
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.
As per discussion above, will leave these in place.
/cc @dsyme |
@@ -354,6 +366,51 @@ private static MethodExecutor WrapVoidMethod(VoidMethodExecutor executor) | |||
return lambda.Compile(); | |||
} | |||
|
|||
private static bool IsAwaitableDirectlyOrViaCoercion( | |||
Type type, | |||
out Expression coerceToAwaitableExpression, |
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 might be time to use a structure 😄
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.
Yeah, and that structure could also wrap up a lot of the Expression generation code as well.
@@ -427,6 +511,17 @@ public ValueTask<string> ValueTaskOfReferenceType(string result) | |||
public void MethodWithMultipleParameters(int valueTypeParam, string referenceTypeParam) | |||
{ | |||
} | |||
|
|||
public FSharpAsync<string> FSharpAsyncMethod(string parameter) |
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 would go further and test an F# project (depending on how hard that is of course)
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 a test project would be great, it would help us see exactly what is being supported, and would also make it easier to consider corner cases for cancellation, exceptions and so on
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 will add an F# test project to the MVC repo and put in a functional test there. That will be a separate PR (since it's a different repo).
Initially it will only cover this specific functionality (invoking actions that return FSharpAsync<T>
) but in the future we can add whatever extra functionality and tests we want.
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 I'm happy to help with that. How do you connect the two repositories? Or are you depending on a MyGet nupkg of this repo?
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.
@panesofglass yeah. The packages get uploaded to MyGet (https://dotnet.myget.org/f/aspnetcore-ci-dev) once they get built and we reference the feed in all our repos.
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.
@panesofglass Thanks for the offer! As it happens I already implemented the basics of this yesterday - it's now in this pull request https://github.com/aspnet/Mvc/pull/6231/files in case you have any suggestions for making it more F#-idiomatic.
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, @SteveSandersonMS! I added a comment to show an async
member, though I doubt you needed that assistance. The F# looks good. Thanks again for pursuing this, and please let me know if I can help by adding test cases. I'm trying to get the Mvc project building locally so that I can do so.
cc @enricosada |
@davidfowl I don't know much about ObjectExecutor or the internals of ASP.NET, but I tweeted here inviting more F# community eyes on this, and perhaps to spark additional work in integrating F# support deeper into ASP.NET infrastructure. It would certainly be really handy if F# async were supported directly in the same places where tasks are supported. You can convert from F# async to/from tasks but it is a little annoying and can occasionally be error prone. Supporting F# async directly is better |
The consuming infrastructure is written in C# and we just need to be able to get the return value of a method. If that method is async, we need to await it before to optionally get the result and continue execution of the pipeline. The |
@dsyme out of curiosity, why isn't |
@davidfowl That's a really good question. The philosophy of F# async is that "an async is a generator of tasks" in the same way that "an IEnumerable is a generator of IEnumerator". That is, we explicitly generate a task or execution for an async (using Async.RunSynchronously or Async.StartImmediate, Async.StartAsTask... etc.) in the same way that we explicitly generate an enumerator for an IEnumerable using GetEnumerator. Composing using generators has many advantages for correct async programming, just like composing with IEnumerable has many advantages (no one programs directly with IEnumerator - it's dangerous because you're directly exposed to the mutable state - just like programming directly with Task is dangerous, because the task is changing status and you can use In short: An F# async is a generator of tasks. Cancellation tokens are provided on task generation and propagated implicitly If we made asyncs implicitly awaitable then it becomes too easy to implicitly start asyncs and impossible to supply the necessary cancellation token when they are started. In the code in this PR, you should be careful to supply the right cancellation token when starting the async. This fundamental difference between F# async and C# tasks is not deeply appreciated (though there are some details in this excellent guide to how C# and F# async differ) and the advantages of auto-propagating cancellation tokens were somehow lost in the long process by which F# async was transferred to C# and now many other languages. To be honest, I think it's really important in practice - I see a lot of complexity (and, I think, mistakes) in code that passes cancellation tokens explicitly and the corresponding F# code generally looks really clean. Having said all this, there may be some way we can make F# async's much nicer to use from C#, e.g. by providing an AsTask instance method that replaces StartAsTask - very happy to have suggestions and PRs :) |
I don't think it's going to be possible to directly use F# Async given @dsyme's explanation above and my own experience adding support for F# Async into ASP.NET Web API several years ago. I think you'll have to stick to the to/from approach. Making F# Async easier to consume in C# should probably get some attention, though, as that would probably be very beneficial to this and other projects. |
@anurse @davidfowl It looks like all the CR feedback is covered. Is there anything else, or can one of you approve this? |
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.
LGTM
I haven't looked at the code - can I just confirm that the cancellation token is being passed in when starting the F# async as a task (which I presume is what's happening in this code at some point), as I mentioned above? |
We don't currently support |
Thanks, reviewers! |
@anurse Thanks. If there's an issue tracking any future work for cancellation tokens then please add a link to this issue. The reflection techniques used in this code would make it very difficult for anyone to ever spot that they might need to do this :) |
@anurse Can you use |
I'm not sure we should pass that through by default. It would mean any of your async operations would be automatically cancelled if the client disconnects. Something like that should be opt in IMO. Anyways this library would need to allow passing a cancellation token and then it becomes a leaky abstraction. |
@davidfowl just curious, but when would you not want to automatically cancel when a client disconnects? |
@panesofglass Example: logging. You wouldn't want to abort logging that something happened just because the client disconnected before your log action completed. |
|
@davidfowl @SteveSandersonMS var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(
this.Response.ClientDisconnectedToken, this.Request.TimedOutToken);
CancellationToken token = tokenSource.Token; |
I don't agree and I think cancellation server side code based on the client should be intentional. That said, this library isn't ASP.NET Core specific and just needs to provide a way to flow some token so that frameworks can pass it to begin with. As for what token we pass in each of the frameworks, that can be debated at some later time on the relevant repository. |
To cover MVC requirements, this extends
ObjectMethodExecutor
so it knows how to treatFSharpAsync<T>
as async and coerce it to an awaitable so that the upstream code gets the correct async result value.