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]: Implicit await and ConfigureAwait #4501

Closed
1 of 4 tasks
dczuo opened this issue Mar 8, 2021 · 2 comments
Closed
1 of 4 tasks

[Proposal]: Implicit await and ConfigureAwait #4501

dczuo opened this issue Mar 8, 2021 · 2 comments

Comments

@dczuo
Copy link

dczuo commented Mar 8, 2021

Implicit await and ConfigureAwait

  • Proposed
  • Prototype: Not Started
  • Implementation: Not Started
  • Specification: Not Started

Summary

As more and more asynchronous adoptions are made, the large amount of await and ConfigureAwait(false) in the code becomes a clear burden.

Motivation

Comparing the implementation of synchronization and asynchronous two ways, asynchronous way appears to be long and difficult to read and difficult to understand.

void M()
{
    var result = M1().M2().M3();

    using var disposableClass = new DisposableClass();

    foreach (var item in new EnumerableClass()) {}
}

async Task M()
{
    var result = await (await (await M1().ConfigureAwait(false)).M2().ConfigureAwait(false)).M3().ConfigureAwait(false);

    var asyncDisposable = new AsyncDisposableClass();
    await using asyncDisposable.ConfigureAwait(false);

    var asyncEnumerable = new AsyncEnumerableClass();
    await foreach (var item in asyncEnumerable.ConfigureAwait(false)) { }
}

Detailed design

Auto-complement await at compile time using AwaitAttribute or #await enable, and complete ConfigureAwait(false) at compile time using ConfigureAwaitAttribute or #configure_await false

[AttributeUsage(AttributeTargets.All, AllowMultiple = true)]
public class AwaitAttribute : Attribute
{
    public string? Scope { get; set; }
    public string? Target { get; set; }
}
#await enable
#await disable
#await restore

[AttributeUsage(AttributeTargets.All, AllowMultiple = true)]
public class ConfigureAwaitAttribute : Attribute
{
	public bool ContinueOnCapturedContext { get; set; }
	public string? Scope { get; set; }
	public string? Target { get; set; }
}
#configure_await false
#configure_await true
#configure_await restore

A simple example

[Await]
async Task M()
{
    var result1 = Task.FromResult(1);       // equivalente var result1 = await Task.FromResult(1)
    #await disable
    var result2 = Task.FromResult(1);       // result2 is Task<int>
    #await restore
    var result3 = Task.FromResult(1);       // equivalente var result3 = await Task.FromResult(1)
}

[ConfigureAwait]
async Task M()
{
    var result1 = await Task.FromResult(1);  // equivalente var result1 = await Task.FromResult(1).ConfigureAwait(false)
    #configure_await true
    var result2 = await Task.FromResult(1);
    #configure_await restore
    var result3 = await Task.FromResult(1);  // equivalente var result3 = await Task.FromResult(1).ConfigureAwait(false)
}

[Await, ConfigureAwait]
async Task M()
{
    var result = Task.FromResult(1);         // equivalente var result = await Task.FromResult(1).ConfigureAwait(false)
}

  • AwaitAttribute and #await enable only work within the method with async modifier
    [Await]
    class C1
    {
        Task M1()
        {
            return Task.Delay(1000);
        }
    
        async Task M2()
        {
            Task.Delay(1000);               // equivalente await Task.Delay(1000);
        }
    }
    
  • ConfigureAwaitAttribute and #configure_await false only works for await target
    [ConfigureAwait]
    async Task M()
    {
        var t1 = Task.Delay(1000);
        await Task.Delay(1000);             // equivalente await Task.Delay(1000).ConfigureAwait(false);
        #await enable
        Task.Delay(1000);                   // equivalente await Task.Delay(1000).ConfigureAwait(false);
        #await restore
     }
    
  • Compatible with explicit await, tip suggests removal
    [Await]
    async Task M()
    {
        await Task.Delay(1000);
    }
    
  • Compatible with explicit ConfigureAwait, tip suggests removal
    [ConfigureAwait]
    async Task M()
    {
        await Task.Delay(1000).ConfigureAwait(false);
    }
    
  • Detecting whether the target of the using statement implements the IAsyncDisposable interface, the IAsyncDisposable interface has been implemented with priority asynchronous method
    class DisposableA : IDisposable {}
    class DisposableB : IDisposable, IAsyncDisposable {}
    
    [Await]
    async Task M()
    {
        using var a = new DisposableA();
        using var b = new DisposableB();            // equivalente await using var b = new DisposableB();
    }
    
  • Compatible with explicit await using, tip suggests removal
    class DisposableA : IDisposable {}
    class DisposableB : IDisposable, IAsyncDisposable {}
    
    [Await]
    async Task M()
    {
        using var a = new DisposableA();
        await using var b = new DisposableB();
    }
    
  • Detecting whether the target of the foreach statement implements the IAsyncEnumerable interface, the IAsyncEnumerable interface has been implemented with priority asynchronous method
    class EnumerableA : IEnumerable {}
    class EnumerableB : IEnumerable, IAsyncEnumerable {}
    
    [Await]
    async Task M()
    {
        foreach (var item in new EnumerableA()) {}
        foreach (var item in new EnumerableB()) {}  // equivalente await foreach (var item in new EnumerableB()) {}
    }
    
  • Compatible with explicit await foreach, tip suggests removal
    class EnumerableA : IEnumerable {}
    class EnumerableB : IEnumerable, IAsyncEnumerable {}
    
    [Await]
    async Task M()
    {
        foreach (var item in new EnumerableA()) {}
        await foreach (var item in new EnumerableB()) {}
    }
    
  • [assembly: Await] Effective within the project
  • [assembly: ConfigureAwait] Effective within the project
  • Scope and Target are defined in the same way as Scope and Target in SuperPressMessageAttribute, and can be defined to the namespace, type, and member levels

Going back to the example in Motivation, that could be rewritten with the use of [Await, ConfigureAwait]

[Await, ConfigureAwait]
async Task M()
{
    var result = M1().M2().M3();

    using var asyncDisposableClass = new AsyncDisposableClass();

    foreach (var item in new AsyncEnumerableClass()) {}
}

If AwaitAttribute and ConfigureAwaitAttribute are declared globally, the pre-method [Await, ConfigureAwait] can also be omitted, and the code within the method remains as concise as it is synchronized.

The above approach is not suitable for all use scenarios, such as the basic class library needs better control of Task, and it is more intuitive to use explicit await when there are not many asynchronous uses. But the advantage of it is that it's optional, doesn't introduce new instruction, doesn't break existing code, and can be ignored when you don't think it's appropriate, using it only in clear and appropriate scenarios (especially for enterprise applications with a large number of repeated CRUD and API calls).

Drawbacks

  • The return value type and the return type in the method definition are inconsistent.
  • The implementation is heavy and difficult

Unresolved questions

The above idea is one of the beautiful wishes of coding life and is not carefully considered.

@HaloFour
Copy link
Contributor

HaloFour commented Mar 8, 2021

See:

#3839
#2649
#2542

@YairHalberstadt
Copy link
Contributor

Closing as duplicate, so that we can focus discussion on the existing issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants