Real-Serious-Games / C-Sharp-Promise

Promises library for C# for management of asynchronous operations.
MIT License
1.19k stars 149 forks source link

Instance of Promise is not awaitable #76

Open lm902 opened 6 years ago

lm902 commented 6 years ago

Make promise object awaitable for consistency with ES2017 standard

ashleydavis commented 6 years ago

Why don't you just use a Task?

RoryDungan commented 6 years ago

In order to be an awaiter an object must implement the INotifyCompletion interface. This is only available in .NET Core, .NET Standard and .NET Framework 4.5+, but we still need to target .NET Framework 3.5 because that is what's used by Unity 3d.

Unity is gradually upgrading to support .NET Standard 2.0 and .NET Framwork 4.6, but only .NET Framework 3.5 is considered "stable" in the current Unity LTS release (2017.4), which will still be supported until 2020. Although Unity 2018.1 supports the new .NET framework, the older version is still the default.

This could be a useful feature but would need to be implemented in a way that still supports the older .NET versions.

MorganMoon commented 6 years ago

I just made an extension method called .Await() for IPromise that uses TaskCompletionSource. It can't be used in the framework officially because TaskCompletionSource is .NET Framework 4+... But, hopefully this helps you @lm902

Here:

    internal class PromiseAwaitable
    {
        private TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();

        public PromiseAwaitable(IPromise promise)
        {
            promise.Done(OnCompleted, OnRejected);
        }

        private void OnCompleted()
        {
            _taskCompletionSource.TrySetResult(true);
        }

        private void OnRejected(Exception ex)
        {
            _taskCompletionSource.TrySetException(ex);
        }

        public Task Run()
        {
            return _taskCompletionSource.Task;
        }
    }

    internal class PromiseAwaitable<T>
    {
        private TaskCompletionSource<T> _taskCompletionSource = new TaskCompletionSource<T>();

        public PromiseAwaitable(IPromise<T> promise)
        {
            promise.Done(OnFinished, OnRejected);
        }

        private void OnFinished(T returnValue)
        {
            _taskCompletionSource.TrySetResult(returnValue);
        }

        private void OnRejected(Exception ex)
        {
            _taskCompletionSource.TrySetException(ex);
        }

        public Task<T> Run()
        {
            return _taskCompletionSource.Task;
        }
    }
}

And the extension methods:

    public static class PromiseExtensions
    {
        public static async Task Await(this IPromise promiseToAwait)
        {
            await new PromiseAwaitable(promiseToAwait).Run();
        }

        public static async Task<PromiseT> Await<PromiseT>(this IPromise<PromiseT> promiseToAwait)
        {
            var awaitablePromise = await new PromiseAwaitable<PromiseT>(promiseToAwait).Run();
            return awaitablePromise;
        }
    }
RoryDungan commented 6 years ago

That's a good idea. I didn't consider using extension methods for this. Once #26 is merged we may be able to use this approach to add the functionality to the main library if there's a clean way to include/exclude these classes depending on the .NET version.

jornj commented 6 years ago

For older .net versions, Microsoft.Bcl.Async might be an option.

RoryDungan commented 6 years ago

I've added support for .NET Standard 2.0 in addition to .NET Framework 3.5 so it should be possible to add this now, as long as it's conditionally disabled so it doesn't break the .NET 3.5 build.

jdnichollsc commented 4 years ago

@MorganMoon hey mate, are you using this library with async/await? :)

MorganMoon commented 4 years ago

@jdnichollsc

@MorganMoon hey mate, are you using this library with async/await? :)

I am using this library in a Unity project, and there are some cases where I use it with async/await, however it is not often. I also use the extension methods below as a way to use a Task as a IPromise

public static class TaskExtensions
    {
        public static IPromise AsPromise(this Task taskToUseAsPromise)
        {
            var promise = new Promise();
            try
            {
                taskToUseAsPromise
                    .ContinueWith((t) =>
                    {
                        if (t.IsFaulted)
                        {
                            // faulted with exception
                            Exception ex = t.Exception;
                            while (ex is AggregateException && ex.InnerException != null)
                                ex = ex.InnerException;
                            promise.Reject(ex);
                        }
                        else if (t.IsCanceled)
                        {
                            promise.Reject(new PromiseCancelledException());
                        }
                        else
                        {
                            // completed successfully
                            promise.Resolve();
                        }
                    }, TaskScheduler.FromCurrentSynchronizationContext());
            }
            catch(Exception ex)
            {
                if(promise.CurState == PromiseState.Pending)
                {
                    promise.Reject(ex);
                }
            }
            return promise;
        }

        public static IPromise<TaskT> AsPromise<TaskT>(this Task<TaskT> taskToUseAsPromise)
        {
            var promise = new Promise<TaskT>();
            taskToUseAsPromise
                .ContinueWith((t) =>
                {
                    if (t.IsFaulted)
                    {
                        // faulted with exception
                        Exception ex = t.Exception;
                        while (ex is AggregateException && ex.InnerException != null)
                            ex = ex.InnerException;
                        promise.Reject(ex);
                    }
                    else if (t.IsCanceled)
                    {
                        promise.Reject(new PromiseCancelledException());
                    }
                    else
                    {
                        // completed successfully
                        promise.Resolve(t.Result);
                    }
                }, TaskScheduler.FromCurrentSynchronizationContext());
            return promise;
        }
    }
jdnichollsc commented 4 years ago

@MorganMoon awesome Morgan! Do you like to create a Pull Request of your changes to have them from the library?

jdnichollsc commented 4 years ago

@RoryDungan hello mate, one little question Can we use C# conditional compilation to detect the version of .NET and apply the extension to support Async/Await?

And guys, What do you think about this https://github.com/Cysharp/UniTask?

RoryDungan commented 4 years ago

Yeah conditional compliation would be a good solution if there's an easy way to set that up. I'd rather not add a dependency on UniTask though because some people do use this library in plain C#/.NET projects outside of Unity, so if there's a way to get that functionality without pulling in the whole other library as a dependency that would be ideal.

JashanChittesh commented 3 years ago

In order to be an awaiter an object must implement the INotifyCompletion interface. This is only available in .NET Core, .NET Standard and .NET Framework 4.5+, but we still need to target .NET Framework 3.5 because that is what's used by Unity 3d.

Is that still the case? I believe the default LTS version of Unity is now 2019.4, which no longer has .NET Framework 3.5. Only 4.5+ and Standard 2.0.

I'm not perfectly sure if this is the right place but I ran into a peculiar situation that may be solved by promises being awaitable: I have a Promise that gets resolved from another thread, which breaks accessing Unity's methods (because the calls no longer come from the main thread). I have a (hacky) workaround by using Promise instead of IPromise, and then looping while CurState is Pending). Something like:

var promise = MethodThatResolvesPromiseFromAnotherThread();
// sync with main thread
while (promise.CurState == PromiseState.Pending) { await Task.Yield(); }
promise.Then(/* Do stuff on Unity's main thread */);

My thinking was that having Promise awaitable would let me do something like:

var promise = MethodThatResolvesPromiseFromAnotherThread();
await promise.Finish();
promise.Then(/* Do stuff on Unity's main thread */);

Would that be possible? Or am I misunderstanding what awaitable Promises are?