-
Notifications
You must be signed in to change notification settings - Fork 1k
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
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
Proposal: Extension await operator to address scoped ConfigureAwait #2649
Comments
Another option would be the ability to override the use of Another option would be the ability to override |
See #1407. But such an approach is both way too heavy for this and also insufficient: there's no way a builder can control how an awaiter invokes a callback. |
That's basically what this is. |
There are some aspects I fundamentally like about this mechanism:
Now, needless to say it also hinges on numerous other concepts. This notion of extension operators: We wouldn't want to do those just for the await operator, but would have to flesh that out as a general feature. Oh, and why only extension operators? We'd want to get into "extension everything" (#192). Also, obviously I also wonder whether the generality of this proposal is worthwhile. Yes, you could use it to extend await for other purposes than So the way I take it is: It's a great idea to keep around, but the path to it has many challenges. Most of the features along the way are interesting, and align with a lot of our thinking. If we did those things for their broader value, this proposal shows that we could get a solution to |
All true. To me this highlights that a solution for ConfigureAwait could naturally fall out of solving all those other things that it would be nice (in most cases) to address anyway (how many times have we uttered "if only we had extension everything"). Of course, there are other ways to functionally achieve the same thing. The proposal is putting forth a strawman syntax, but at the end of the day, all it's really doing is providing a way to hook an await. And we already have such a mechanism, GetAwaiter, so essentially this is nothing more than a glorified syntax for writing an extension GetAwaiter method. The rub is that it needs to take precedence over the existing GetAwaiter instance methods that exist today, and I think we can all agree that changing that precedence would be a bad thing; not only would it a massive, unacceptable breaking change to do for all extension methods, it'd be a breaking change to do for just GetAwaiter, and even if we decided that was ok, special-casing it for just GetAwaiter feels very wrong. Of course, we could introduce another aspect to the awaiter pattern: a type is awaitable not only if it exposes GetAwaiter returning the right shape, but alternatively if it exposes a GetAwaitable which itself returns a GetAwaiter. All of the operators in my original proposal just become extension methods: internal static class ConfigureAwaitExtensions
{
internal static ConfiguredTaskAwaitable GetAwaitable(Task t) => t.ConfigureAwait(false);
internal static ConfiguredTaskAwaitable<T> GetAwaitable<T>(Task<T> t) => t.ConfigureAwait(false);
internal static ConfiguredValueTaskAwaitable GetAwaitable(ValueTask vt) => vt.ConfigureAwait(false);
internal static ConfiguredValueTaskAwaitable<T> GetAwaitable<T>(ValueTask<T> vt) => vt.ConfigureAwait(false);
} which of course already exist as a concept, can be generic, etc. And we would suggest that types themselves not expose their own instance GetAwaitable, but instead leave it as something for others to implement in order to hook awaits (we could even go so far as to say that await doesn't consider instance GetAwaitable methods :)). |
@stephentoub Yeah, that seems like a much lower cost approach to getting the problem solved, with most of the same basic properties. Of course relying on another (extension) method to take precedence is still a breaking change, because that method could exist today, and doesn't take precendence! 😄 |
Yeah, was hoping you wouldn't notice that ;) That's actually from my perspective a key benefit of the operator approach: you couldn't have written them today. There are of course ways around that, e.g. introducing a new attribute that doesn't exist today and requiring the method to be attributed. And while, sure, someone could have had both that method and attribute in their own code and had the latter on the former, I'm willing to accept that break (and we could do the whole poisoning thing if we weren't). |
There is no need to do so. The builder can remove or replace the synchronization context for the operation before the awaiter is created. I'm not saying one way is better or worse, just presenting alternatives which could achieve the same final outcome. |
Not without changing other semantics the method may be relying on. And there's also the TaskScheduler that awaited tasks pay attention to. And other schedulers that other awaited things may pay attention to. And so on. |
There are two reasons I don't like this as extension operators. First is that there is seemingly one flavor of how these operators would be implemented, so it'd either belong in the BCL, or it would need to be in a common NuGet package, otherwise everyone will end up reimplementing them over and over again. Second is that as extension operators I can only assume that they would need to be imported via the appropriate namespaces in scope, which makes the solution relatively brittle across a codebase. Accidentally miss a The attribute approach just seems cleaner to me, both for the compiler and for the developer. Assuming that it can target |
Which attribute approach? I've yet to see one that's actually viable. |
That seems to be a conversation fraught with opinion. I don't see any problems with the viability of a |
That applies to what types? |
IMO? Just spitballing, but I think I'd allow it to target I don't doubt that there are problems with this approach, conceptually and from an implementation perspective. But it feels like it involves significantly fewer moving parts than an |
I agree with @HaloFour. Having the behavior of your Having an assembly level targeted attribute which sets the default I don't really buy the argument that the compiler or framework specially targeting |
using for what? There's no namespace in my example. It's no different from the attribute in that regard: either you include the code or you don't. The arguments about it being easier to prove correct do not resonate with me.
From my perspective, ConfigureAwait is an ugly but necessary workaround we should not be propagating into the C# language, which the attribute does. I disagree that the attribute is "cleaner".
I get asked about wanting this ability on some what regular basis. Wanting to make all awaits cancelable via a global token. Wanting a timeout on all awaits. Wanting additional logging around all awaits. Wanting to flow additional state (e.g. in specific thread locals) across awaits. Wanting to force all awaits to complete asynchronously. Wanting to schedule all continuations to a specific scheduler. Wanting to override the SyncCtx/TaskScheduler behavior to instead look for a different ambient scheduler. And so on. And where the "all" here might be for an assembly, or a particular class. |
If it's put in the global namespace, then if someone imported it into a nuget package it would effect every package that consumes that package wouldn't it? |
There has to be, otherwise you screw up everyone's notion of
I also agree, but here we are.
|
Why would
This is simply not true. |
There doesn't have to be. You can have going ones for the whole project, and ones in namespaces to pull in specific ones. |
This is assuming everybody copies and pastes the code into their project manually. I think it highly likely someone, somewhere will create a package where this extension method is public and in the global namespace, and it will infect a lot of downstream assemblies. Is this a risk you are willing to take? |
So the expectation is that every developer will duplicate their own versions of these operators? How about this, have the That came off kind of snarky and I apologize. Actually, if all of the proposals required to make extension |
I fail to see how this specific point would be an issue. There are already plenty of ways a nuget package can affect a whole application. If a library does that and this is not a desirable behavior for you, you simply stop using that library and pick another one. |
For those points it would be even better to have some kind of scoping mechanism, to be able to also affect the external libraries (and the BCL) as well. But I agree it would already be a huge improvement. |
Indeed. The risk here is it's something that is easy to do, seemingly innocuous, and quite hard to detect. The issue is you're telling everybody whose writing a library that they have to copy and paste this specific file into every single project they have. Somebody is definitely going to have the bright idea of simply making it public, and then everyone who depends on this library will have their behaviour subtly changed. I'm not saying this is definitely going to be a disaster. I'm saying that this probably makes it a lot more likely people will do the wrong thing than other features that have the ability to effect every consumer. The risk ought to be considered, even if it's decided the benefits are worth it. |
If we are willing to make language changes, perhaps we could introduce an alternative await syntax operation that performs the call to We could use a different keyword, or introduce modifiers, like It would avoid semantic changes when importing a namespace. |
Perhaps tweak the |
For what it's worth, I explored this possibility here: #645 (comment) |
Is it for the ConfigureAwait(false) part not possible to have something like the Nullable reference types which is set by
Some extension that will have that habit change sounds really scary and pretty sure when it is used to change the default behavior of Could the methods that are invoked by |
If we'll go with this "extension operators" route this will be a good opportunity to add other operators. For example string interpolation. I remember there was huge discussion whether it should use current culture or invariant. It was decided it is current culture and some people really didn't like it. With extension operators we could define something like this to get invariant behavior: internal static class StringInterpolationExtensions
{
internal static string operator $(FormattableString fs) => fs.ToString(System.Globalization.CultureInfo.InvariantCulture)
} |
I strongly agree with @canton7 and @Duranom that the ideal solution is much higher level than the await operator. From a developer's point of view, the decision about whether to capture synchronization context or not is almost never made at the statement level. It is almost always made, for very good reasons, at the level of the containing async method, class, or project. The reason everyone is complaining about this is because they are currently required to express that very high-level decision on every affected statement. It would be like having to say "use invariant culture for string comparisons and date formats" at the statement level, when your entire project has nothing to do with locale (oh wait, that IS what we have to do 😄). If I had to choose, I'd want a project-level setting first, then maybe an With a project-level settings,
I don't entirely follow all the arguments about why various solutions to this problem may or may not be hard at the language or compiler level. I just want to emphasize that any solution should try very hard to match the scope at which developers actually make this decision in real life. |
Reading this discussion gave me an idea for another take on this. Using attributes was mentioned as a problem due to putting to much knowledge in the compiler. But perhaps this could be avoided with a more generic approach to the same idea. Similar to how the compiler can pass down CallerMemberName currently it could pass down any implicit context required by the invoked code. The BCL ConfigureAwait methods could then implement the desired scope based configuration by declaring the required scope, and the compiler would make it available. It seems awfully close to the implicits feature of Scala at this point though. Which, if I understand correctly, isn’t universally seen as a success story. But perhaps there some value to it. I’m not sure if the scope is best configured dynamically or lexically though, so leaving that part out for now. The point mainly being to point at another generic way for the compiler to allow some scoped configuration with minimal syntax without knowing anything about tasks. |
Scala implicits are often one of those incomprehensible things about that language. It's way too easy for one import to silently and subtly change the behavior of your code. Actually, in a way, that makes it very similar to this proposal. The TPL context currently flows through thread locals. The awaiter pattern is ignorant that they exist, they only apply within the workings of the |
Yeah, that was kind of what I was hinting at. But perhaps there’s a way to rein things in to be less of a foot gun and more of a support. In your example the issue is that an import brings in some magic change. I was thinking of this thread local as currently magic in same way. The goal would be to make things less magic in a way where the compiler would loudly complain about the missing context. So when you access a member requiring a certain scope, mutating a GUI-control f.ex. It would have an attribute hinting at the compiler that it requires a certain scope. The compiler could simply demand that the invoking method has the same attribute before allowing things to compile. This would then taint the method in a similar manner as returning Task does, pushing the issue up the call stack. But yes the idea would be to make it generic enough to not be tied to async/await. I was thinking this thing could be generic enough to allow another take at checked exceptions f.ex. Or other thread safety issues like demanding a certain lock to be held and such things. There would be changes in BCL of course, but perhaps that can be done in an additive way by simply adding overloads in a few places. The nullable reference types seems to have landed in good place, hopefully this effort could be handled in a similar way. |
Please do this. My vote is for extensions methods + attribute that allows changing the precedence of overload selection. I would limit it to the current assembly only (i.e. the extension method and method where precedence is changed need to be in the same assembly.) It can be useful for other scenarios too. |
An alternative to this would be a VS extension that applies
An extension go a step further than an analyzer could go by directly applying code changes without alert or confirmation. |
I don't believe adding it automatically would be beneficial at all, because this would still pollute the code and make it much less clear. Any real solution is something that will clear the code from such clutter (like most of those suggested along this thread) |
While I am having a really hard time wrapping my head around "extension operators", I mean compared to extension methods, which are fairly obvious. They are basically just syntactic sugar for invoking a static method, and If you have a conflict then it's easy to solve by invoking a static method. Also, as far as my understanding goes, the compiler just turns them straight into calls to the static methods. Which means that you couldn't push these to other assemblies unless if you where explicit about this. What are the answer to all of these questions for extension operators? o.O... That being said, lets imagine for a moment that we could either do what was proposed here ( or if the Extension method way could be made to work in some way without breaking existing code, that would be better IMO ) - Couldn't this then be turned into a assembly attribute using Source Generators?... That combination sounds like a somewhat pragmatic approach to me and I would be more than happy about that. |
I really like the concept of extension await operators, and I think it would also go really well with #4565 if it gets implemented. While the extension await operator itself might not work (how can you resolve ambiguities?), I like that it's not type or method constrained. I would also be fine with an attribute approach, but that should be generic enough to transform awaitables to any other awaitable type, not just ConfigureAwait. Also, with this await override enabled, how could you override the override to await the normal way if you needed to? |
I do something similar to this except I have an extension method named |
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
Library developers are often frustrated at needing to use
.ConfigureAwait(false)
on all of theirawait
s, and this has led to numerous proposals for assembly-level configuration, e.g. #645, #2542.There are, however, concerns with such specific solutions.
await
is pattern based, and theConfigureAwait
instance methods exposed by{Value}Task{<T>}
aren't known to the language or special in any way: they just return something that implements the awaiter pattern. And not everything that is awaitable exposes such aConfigureAwait
method, so creating a global option that somehow specially-recognizesConfigureAwait
is very constraining.I instead propose a way we can address the
ConfigureAwait
concerns, with minimal boilerplate, while also being flexible enough to support other scenarios, provide some level of scoping beyond assembly level, etc.Proposal
We introduce the notion of extension operators, and in particular an extension await operator. Such extension await operators would be used by the compiler to get the actual awaitable instance any time an await is issued, whether explicitly by the developer or implicitly by the compiler as part of a language construct like
await foreach
orawait using
.As a developer, I can add such extension operators to my project, e.g.
Following normal scoping rules, any
await
that sees these in scope will then use them to determine the actual instance to await, e.g. code that does the following:and that has the relevant extension operator in scope will be compiled instead as:
That way, I can have a file containing such extensions, include it in my project, and all of my awaits in my project will then subscribe to the relevant behavior. It's also not limited to just working with a fixed set of types, with operators being writable for any type a developer may want to
await
, even if it doesn't directly expose the awaiter pattern. And such extensions need not be limited to callingConfigureAwait(false)
, but could perform arbitrary operations as any operator can. By changing where the operators are defined, I can also limit their impact to just a subset of my project, as is the case for extension methods.cc: @MadsTorgersen
The text was updated successfully, but these errors were encountered: