Skip to content
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

Open
stephentoub opened this issue Jul 11, 2019 · 45 comments
Open

Comments

@stephentoub
Copy link
Member

stephentoub commented Jul 11, 2019

Library developers are often frustrated at needing to use .ConfigureAwait(false) on all of their awaits, 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 the ConfigureAwait 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 a ConfigureAwait method, so creating a global option that somehow specially-recognizes ConfigureAwait 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 or await using.

As a developer, I can add such extension operators to my project, e.g.

internal static class ConfigureAwaitExtensions
{
    internal static ConfiguredTaskAwaitable         operator await   (Task t)          => t.ConfigureAwait(false);
    internal static ConfiguredTaskAwaitable<T>      operator await<T>(Task<T> t)       => t.ConfigureAwait(false);
    internal static ConfiguredValueTaskAwaitable    operator await   (ValueTask vt)    => vt.ConfigureAwait(false);
    internal static ConfiguredValueTaskAwaitable<T> operator await<T>(ValueTask<T> vt) => vt.ConfigureAwait(false);
}

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:

Task t = ...;
await t;

and that has the relevant extension operator in scope will be compiled instead as:

Task t = ...;
await ConfigureAwaitExtensions.<>__await1(t); // compiler-generated name for the operator

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 calling ConfigureAwait(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

@stephentoub stephentoub added this to the 8.X candidate milestone Jul 11, 2019
@stephentoub stephentoub self-assigned this Jul 11, 2019
@sharwell
Copy link
Member

sharwell commented Jul 11, 2019

Another option would be the ability to override the use of AsyncTaskMethodBuilder (or similar for other return types) by the compiler's lowering features. Something like an assembly-level AsyncMethodBuilderAttribute that could be applied.

Another option would be the ability to override GetAwaiter() used for pattern-based await operations, but I'm not sure what form that would take to not be concerning.

@stephentoub
Copy link
Member Author

stephentoub commented Jul 11, 2019

Something like an assembly-level AsyncMethodBuilderAttribute that could be applied.

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.

@stephentoub
Copy link
Member Author

Another option would be the ability to override GetAwaiter() used for pattern-based await operations

That's basically what this is.

@MadsTorgersen
Copy link
Contributor

There are some aspects I fundamentally like about this mechanism:

  • it intercepts the await in a scope-based manner
  • it uses language level constructs to do so
  • it avoids undue knowledge of types or members in the compiler/language

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 await is not an overloadable operator today, so what does that look like? Also, it would have to be generic (to pass through the result type of Task<T>, ValueTask<T> etc), and we don't have a notion of generic operators today, so what does that look like? And so on. All in all there's a lot to figure out, and a lot of adjacent features to agree on, before this becomes solid.

I also wonder whether the generality of this proposal is worthwhile. Yes, you could use it to extend await for other purposes than ConfigureAwait. But if you do, does that compose with also doing it for ConfigureAwait? Presumably you can't have more than one extension await operator applying to a given type at a given point in the code.

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 ConfigureAwait as an additional benefit.

@stephentoub
Copy link
Member Author

needless to say it also hinges on numerous other concepts

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 :)).

@MadsTorgersen
Copy link
Contributor

@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! 😄

@stephentoub
Copy link
Member Author

stephentoub commented Jul 11, 2019

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).

@sharwell
Copy link
Member

sharwell commented Jul 11, 2019

there's no way a builder can control how an awaiter invokes a callback

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.

@stephentoub
Copy link
Member Author

The builder can remove or replace the synchronization context for the operation before the awaiter is created.

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.

@HaloFour
Copy link
Contributor

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 using statement and you're doing the wrong thing. And that namespace would have to be fairly unique with nothing else of interest lest you accidentally change the synchronization behavior just by pulling in some other types.

The attribute approach just seems cleaner to me, both for the compiler and for the developer. Assuming that it can target module/assembly it's set once and forget it. This operator approach seemingly opens several new cans of worms around treating await as an operator in general, extension operators, generic operators, etc., all of which has already been pointed out.

@stephentoub
Copy link
Member Author

The attribute approach just seems cleaner to me

Which attribute approach? I've yet to see one that's actually viable.

@HaloFour
Copy link
Contributor

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 ConfigureAwaitAttribute(bool), and it seems to be a lot less complicated than the five or so orthogonal proposals necessary to get an extension generic await operator even off the ground, just so that it can accomplish one thing. Even better, the attribute approach could be 100% accomplished via source generators, if they ever become a thing.

@stephentoub
Copy link
Member Author

I don't see any problems with the viability of a ConfigureAwaitAttribute(bool)

That applies to what types?

@HaloFour
Copy link
Contributor

@stephentoub

That applies to what types?

IMO? Just spitballing, but I think I'd allow it to target assembly/module, class/struct and method. The compiler/generator would inspect the current async method for the attribute and if it was not defined there would check the declaring type then the module/assembly. If the attribute is found the compiler/generator would attempt the pattern .ConfigureAwait(bool).GetAwaiter() rather than .GetAwaiter().

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 await operator. I'd like to hear about other flavors of an await operator other than one that calls ConfigureAwait(false).

@MgSam
Copy link

MgSam commented Jul 12, 2019

I agree with @HaloFour. Having the behavior of your awaits based on the presence/non-presence of a using statement at the top of the file seems like a recipe for mistakes. Sure, an analyzer could help here but I think the whole UX wouldn't be very good. You miss one file and you have a hard-to-find bug hiding in your code.

Having an assembly level targeted attribute which sets the default ConfigureAwait for the whole assembly is far cleaner. You do it once, and you have confidence in the behavior everywhere.

I don't really buy the argument that the compiler or framework specially targeting ConfigureAwait is too over-constrained. This is a construct that is literally used everywhere, and substantially adds to the burden of using async correctly in C#/.NET. IMO, making ConfigureAwait(true) the default was a mistake, and providing a better workaround for that mistake as cleanly as possible (and sooner rather than later) trumps any theoretical argument for increased generality.

@stephentoub
Copy link
Member Author

stephentoub commented Jul 12, 2019

presence of a using statement

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.

specially targeting ConfigureAwait

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'd like to hear about other flavors of an await operator other than one that calls ConfigureAwait(false).

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.

@YairHalberstadt
Copy link
Contributor

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.

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?

@HaloFour
Copy link
Contributor

HaloFour commented Jul 12, 2019

@stephentoub

There's no namespace in my example.

There has to be, otherwise you screw up everyone's notion of await and you can't have different custom versions within a project.

From my perspective, ConfigureAwait is an ugly but necessary workaround we should not be propagating into the C# language, which the attribute does.

I also agree, but here we are.

I get asked about wanting this ability on some what regular basis.

  1. All things that you can accomplish via a custom synchronization context.
  2. All things that you can't manage well with extension methods because of the fact that you're pulling them into an entire source file/namespace by using statements.

@stephentoub
Copy link
Member Author

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?

Why would internals be visible outside the assembly?

All things that you can accomplish via a custom synchronization context.

This is simply not true.

@stephentoub
Copy link
Member Author

There has to be, otherwise you screw up everyone's notion of await and you can't have different custom versions within a project.

There doesn't have to be. You can have going ones for the whole project, and ones in namespaces to pull in specific ones.

@YairHalberstadt
Copy link
Contributor

YairHalberstadt commented Jul 12, 2019

Why would internals be visible outside the assembly?

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?

@HaloFour
Copy link
Contributor

HaloFour commented Jul 12, 2019

@stephentoub

Why would internals be visible outside the assembly?

So the expectation is that every developer will duplicate their own versions of these operators?

How about this, have the ConfigureAwaitAttribute cause the compiler to emit the internal versions of those operator extensions? At least then we can avoid having tons of broken/buggy implementations floating around.

That came off kind of snarky and I apologize.

Actually, if all of the proposals required to make extension await a thing happen as well as source generators, that sounds like it might actually be a good way to have those operators generated.

@kevingosse
Copy link

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?

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.

@kevingosse
Copy link

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.

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.

@YairHalberstadt
Copy link
Contributor

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.

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.

@john-h-k
Copy link

We introduce the notion of extension operators

Outside of the specific await overload-ability, I really like this idea overall

@john-h-k
Copy link

john-h-k commented Jul 12, 2019

To address the worries it could be used to "mess up" the task types, it could just be that {Value}Task{<T>} internally defines the operators - then extension ones would never be invoked

public partial {class|struct} {Value}Task{<T>}
{
    public bool IsAwaitConfigured { get; set; } = false;
    public static {void|T} operator await({Value}Task{<T>} task)
    {
        task.ConfigureAwait(IsAwaitConfigured);
    }
}

@bartdesmet
Copy link

I do like the ability to intercept await as an operator one way or another. Recently, I've been working on a system in C++ using upcoming support for coroutines in C++20, which offers ample extensibility points we've been using to thread through schedulers and mechanisms akin to ExecutionContext to flow state across co_await sites.

Three things have been particularly useful and are relevant to this conversion:

  • Binding of co_await e first looks for an await_transform method on the promise type, which is analogous to the async method builder type in .NET. It allows to transform an awaiter and co_await the result returned by await_transform.
  • Binding of operator co_await, which is the equivalent of GetAwaiter and can either be found either as a member on non-member overload, akin to support for binding GetAwaiter as an extension method. The main difference is that co_await is an operator.
  • Ability to bind to an "awaiter" without the presence of an operator co_await, which would be equivalent to await being willing to bind to an object that has the shape of what's returned by GetAwaiter today.

Quoting @stephentoub on this possibility:

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.

This is very similar to the latter two extensibility points in C++ coroutines, effectively adding two chances for binding await e. In C++, there are three levels: e.operator co_await() as a member, operator co_await(e) as a non-member, or e (requiring the "awaiter" pattern). In the method-based approach for C#, there'd be two levels: e.GetAwaitable() returning an awaitable and e.GetAwaiter() returning an awaiter.

For the (extension) operator-based approach, I assume the thinking is that a regular (non-extension) operator await would be added to the language, but types such as [Value]Task[<T>] would not have such an operator defined (as a public static … operator await() on those types themselves).

Then, if an extension operator await is defined, it takes precedence. If no operator await exists at all, the existing rules apply to detect the awaitable pattern. So the rules would be similar to the GetAwaitable approach above:

  1. find operator await on the type of the await operand (~ GetAwaitable instance method, or operator co_await as a member lookup in C++);
  2. find an extension operator await based on the new concept of extension operators (~ GetAwaitable extension method, or operator co_await as a non-member lookup in C++);
  3. existing rules to treat the await operand as having the awaitable pattern (~ GetAwaiter, or treating the object itself as an "awaiter" in C++).

Without a "regular" operator await being supported to be defined on a type (just like any existing unary operator definition), we'd have some new notion of an operator that only exists as an "extension operator". This would also feel like an extension operator await taking precedence over an instance GetAwaiter, which is backwards compared to existing extension methods being the last resort for binding.

FWIW, another extensibility point that could be considered is an analogous concept to await_transform on promise types in C++, which we've been using extensively to intercept co_await sites in a coroutine method, e.g. to capture and restore context across suspension points, but also to implement a coroutine scheduling scheme. (In fact, we combine this with "parameter preview" capabilities on coroutine methods to fish out an allocator from the parameters on the coroutine method, which is similar to weaving a concept like cancellation through async methods.)

The C# analogous concept to this would be some instance method on the async method builder, which does not exist in the BCL by default, thus opening up for it to be defined as an extension method. While potentially more flexible (i.e. the ability to substitute one awaitable for another one, e.g. wrapping the original one), it likely gets unweildy quickly because on needs to define extension methods like this:

static ConfiguredTaskAwaiter<T> GetAwaiter<R, T>(this AsyncTaskMethodBuilder<R> builder, Task<T> task) {}

which has way more combinations than the four task variants because it also involves the return type of the async method (for which we have five distinct builder types), unless there's some common (interface) type across all builder types to tame this:

static ConfiguredTaskAwaiter<T> GetAwaiter<Builder, T>(this Builder builder, Task<T> task) where Builder : IAsyncMethodBuilder {}

In a way, it's similar to AwaitOnCompleted methods on the builder operating on the awaitees within the async method, though it'd serve a different complementary purpose. For plain ConfigureAwait it's most likely overly complex (and users have to see the System.Runtime.CompilerServices builder types they likely have never heard of), but it could be a design point if other behavior adapters for await are desirable and could benefit from context provided by the builder. Quoting @stephentoub:

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.

Things like cancellation, timeouts, schedulers, etc. seem like they would likely be threaded through async methods as parameters rather than being globals (though async locals may be an alternative for a subset of those in some cases). At that point, these things become contextual, and the nearest relevant context for an await site is the containing async method, so getting a handle to that would be desirable.

Obviously, the question becomes how to "fish out" parameters and have them be accessible to the transformer of the awaitees. The [EnumeratorCancellation] attribute for async enumerables is in fact a bit similar and a very specific case of this more general pattern; it carries a top-level parameter down to a synthesized construct. (For comparison, in the C++ land, one uses a variadic template to preview the parameters of the coroutine and uses template metaprogramming techniques to fish out things, the simplest of which is the "leading allocator convention" which feels very similar to the "trailing cancellation token convention" in .NET.)

In fact, await foreach is another place where ConfigureAwait(false) can occur in the body of an async method, this time on an IAsyncEnumerable<T> to influence all await sites. It'd be great for whatever proposal on implicit ConfigureAwait application to cover this case as well. It may just fall out from the await expressions on the ValueTask<bool> and ValueTask values returned from await foreach lowering being subject to binding rules that pick up on operator await or some GetAwaitable extension, but it would involve more binding steps after initial lowering of await foreach.

Alternatively, one could think of it as await foreach being "transformed" in the context of the surrounding async (iterator) method, thus allowing transformations for ConfigureAwait or passing of cancellation tokens to be applied to the source operand of await foreach (prior to lowering). With an await_transform type of thing, it could be (yet another) overload for IAsyncEnumerable<T> that returns the configured variant (or, with the ability to pick up on more async method context, the WithCancellation variant of the sequence to thread cancellation down).

Just one final thought from more noodling with C++ coroutines lately. We've been using initial_suspend and final_suspend as extensibility points as well, in combination with await_transform. One place where this became handy is to track execution of async coroutine methods (when they get kicked off, when they get suspended due to a co_await, and when they complete), both for diagnostics (async call stacks, async "task manager" to see what's running and how much time is spent, etc.) but also for scheduling decisions (e.g. one can force a co_await to suspend based on the containing coroutine's runtime relative to other coroutines running on the same scheduler, thus forcing a "context switch").

Wanting a timeout on all awaits.

Timeouts reminded me of this due to the similarity with our accounting voodoo. Sometimes one wants to compute timeouts from an initial budget (the timeout of the overall operation, no matter how many await sites it has) and actual execution time, rather than having all awaits being subject to the same timeout. Support for some contextual "await transformer" akin to await_transform could be use to transform each await e by an await e.WithTimeout() where WithTimeout is similar to Task.WhenAny(e, Task.Delay(t)) but also takes care of getting the remaining timeout t (e.g. from an async local) and adjusting it upon resumption).

All in all, I think having a look at C++ coroutines may be worth it as just another data point. The degree of extensibility over there is quite high, some of which may not carry over to C# (or not be desirable at all) but could provide another point of view that could be useful here. I, for one, found all the knobs provided over there to be useful.

@gafter gafter removed this from the 8.X candidate milestone Jul 15, 2019
@dsaf
Copy link

dsaf commented Jul 15, 2019

#2488

@migueldeicaza
Copy link

If we are willing to make language changes, perhaps we could introduce an alternative await syntax operation that performs the call to ConfigureAwait(false) on our behalf. It is an important enough concept that it might belong in the language.

We could use a different keyword, or introduce modifiers, like await explicit that achieve this.

It would avoid semantic changes when importing a namespace.

@canton7
Copy link

canton7 commented Jul 15, 2019

We could use a different keyword, or introduce modifiers, like await explicit that achieve this.

Perhaps tweak the async rather than the await? Since it's rare to have a mix of ConfigureAwait(true) and ConfigureAwait(false) in the same method, so something method-wide would be potentially less error-prone.

@kevingosse
Copy link

If we are willing to make language changes, perhaps we could introduce an alternative await syntax operation that performs the call to ConfigureAwait(false) on our behalf. It is an important enough concept that it might belong in the language.

We could use a different keyword, or introduce modifiers, like await explicit that achieve this.

It would avoid semantic changes when importing a namespace.

For what it's worth, I explored this possibility here: #645 (comment)

@MadsTorgersen MadsTorgersen moved this from TRIAGE NEEDED to X.0 Candidate in Language Version Planning Jul 17, 2019
@gafter gafter added this to the X.0 candidate milestone Jul 17, 2019
@Duranom
Copy link

Duranom commented Jul 21, 2019

Is it for the ConfigureAwait(false) part not possible to have something like the Nullable reference types which is set by <Nullable>enable</Nullable> in the project file and let the compiler override the default behavior? This is something that is most common I think and a simple more option like that would be nice.

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.

Some extension that will have that habit change sounds really scary and pretty sure when it is used to change the default behavior of await will blow a lot of .NET developer minds and create a lot, a lot, of misunderstandings (again). Async await was for some reason really hard to grasp by many and with a overriding operator feature like this I sense a lot of sneaky issue's arising, it feels a bit like changing the operator for a built-in type. If this will be ever added than would really like an unsafe tag there or something.

Could the methods that are invoked by await in an awaiter be expanded/new ones added that provide additional information or steps that want to be intercepted by those requests? (And maybe give/require awaiters an interface so some sort chaining extension methods with decoration pattern could be done?)
Or a new await syntax where you can pass a type for configuration like await CustomAwaitBehaviors for Task.Run(() => Foo());, so you are still expressive but can have a single configuration for all or even different configuration depending on the needs at that moment.

@mpawelski
Copy link

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)    
}

@bobmeyers5
Copy link

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 async modifier to override that behavior for all await calls within a single async method. If I had these two options, I would probably never use a statement-level language feature.

With a project-level settings, async modifiers would need to go both directions like this:

public async nocontext Task DoSomethingAsync()
public async withcontext Task DoSomethingAsync()

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.

@JohnNilsson
Copy link

JohnNilsson commented Mar 23, 2020

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.

@HaloFour
Copy link
Contributor

@JohnNilsson

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. :trollface:

The TPL context currently flows through thread locals. The awaiter pattern is ignorant that they exist, they only apply within the workings of the TaskAwaiter class that follows the pattern that C# recognizes. Having to flow implicits through would require reworking all of that machinery, and would also require virally changing the signature of pretty much all async methods to now accept it as a parameter. And the only way to apply it silently would be for the awaiter pattern to be aware of this context being in scope, which it currently isn't. And whatever context the awaiter pattern would recognize would have to be generic enough to not tie it specifically to the TPL.

@JohnNilsson
Copy link

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.
(Hmm sounds awfully much like the monads of Haskell now...)

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.

@aelij
Copy link

aelij commented Jul 29, 2020

Please do this. ConfigureAwait is one of the most frustrating things about C# today IMO, and is the source of many subtle bugs, especially when consuming a library that doesn't use ConfigureAwait properly.

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.

@GSPP
Copy link

GSPP commented Jul 29, 2020

An alternative to this would be a VS extension that applies ConfigureAwait(false) automatically as you type without intervention. It would be configured by an attribute:

[assembly: ApplyConfigureAwaitFalseAutomatically]

An extension go a step further than an analyzer could go by directly applying code changes without alert or confirmation.

@lohoris-crane
Copy link

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)

@jeme
Copy link

jeme commented Nov 16, 2021

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.

@timcassell
Copy link

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?

@TonyValenti
Copy link

I do something similar to this except I have an extension method named DefaultAwait() that I call after every await. In some assemblies, DefaultAwait() is ConfigureAwait(false) and some it is ConfigureAwait(true).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Language/design
Development

No branches or pull requests