A Typescript-like Promise library for .NET.
This library is being deprecated in favor of doomchild/task-chaining, so you should probably switch to that. The reason for the deprecation is because this library needlessly wraps Task
instances in a Promise
object that just proxies down into the Task
without actually doing anything useful. The other library is also less confusing in its overloads, because it only needs to handle raw values and Task
s instead of needing to handle three different possibilities (raw, Promise
-wrapped, and Task
-wrapped). It's just generally better.
Asynchronous code (particularly in C#) typically relies on using the async
/await
feature introduced in C# 5.0. This has a lot of benefits, but it unfortunately tends to push code into an imperative style. This library aims to make writing asychronous functional code easier, cleaner, and less error-prone.
Install RLC.Promises as a NuGet package, via an IDE package manager, or using the command-line instructions at nuget.org.
This interface is the main interface exported by the library, and should be used as the return value from any Promise-returning functions. Note that IPromise<T>
instances can be awaited like Tasks
, but cannot be passed to functions expecting Task<T>
instances, as awaiting is not done by descending from an interface or base class, but rather by implementing the "magic" method GetAwaiter
.
There are three ways to create a Promise<T>
instance.
To create a resolved Promise
from a raw value, Resolve
is the easiest way to go.
Resolve
will take a Task<T>
. However, if the Task<T>
throws an exception, the resulting Promise<T>
will be in a rejected state with the exception that was thrown.
To create a rejected Promise
from an exception object, Reject
is the easiest way to go.
RejectIf
can be used to create a Promise
based on some function, or can be used for validation, as follows:
Promise<int>.Resolve(1)
.Then(Promise<int>.RejectIf(value => value % 2 == 0, value => new ArgumentException($"{nameof(value)} was not even!")))
.Tap(
value => _logger.LogInformation("Value was even: {Value}", value),
exception => _logger.LogException(exception)
);
The Promise<T>
constructor takes an Action<Action<T>, Action<Exception>>
to allow callback-oriented code to resolve or reject a value/exception in order to enter a Promise<T>
-oriented workflow. This is analogous to Javascript/Typescript Promise
s, where you might occasionally need to write something along the lines of:
new Promise((resolve, reject) => {
try {
performCallback(resolve);
} catch (Error error) {
reject(error);
}
}
Once a Promise<T>
has been created, successive operations can be chained using the Then
method.
HttpClient client = new ();
Promise<string>.Resolve("https://www.google.com/")
.Then(client.GetAsync)
.Then(responseMessage => responseMessage.StatusCode);
When a Promise<T>
enters a rejected state, the Catch
method can be used to deal with the exception.
HttpClient client = new ();
Promise<string>.Resolve("not-a-url")
.Then(client.GetAsync)
.Catch(exception => exception.Message)
.Then(message => message.Length);
The IfFulfilled
and IfRejected
methods can be used to perform side effects such as logging when the Promise<T>
is in the fulfilled or rejected state, respectively.
HttpClient client = new ();
Promise<string>.Resolve("https://www.google.com/")
.Then(client.GetAsync)
.IfFulfilled(response => _logger.LogDebug("Got response {Response}", response)
.Then(response => response.StatusCode);
HttpClient client = new ();
Promise<string>.Resolve("not-a-url")
.Then(client.GetAsync)
.IfRejected(exception => _logger.LogException(exception, "Failed to get URL")
.Catch(exception => exception.Message)
.Then(message => message.Length);
The Tap
method takes both an onFulfilled
and onRejected
Action
in the event that you want to perform some side effect on both sides of the Promise
at a single time.
HttpClient client = new ();
Promise<string>.Resolve(someExternalUrl)
.Then(client.GetAsync)
.Tap(
response => _logger.LogDebug("Got response {Response}", response),
exception => _logger.LogException(exception, "Failed to get URL")
)
Promise<T>
has some useful static methods for dealing with multiple Promise<T>
s or Task<T>
s at once.
This method takes an IEnumerable<Task<T>
of existing Task<T>
instances or an IEnumerable<Func<Task<T>>>
of Task<T>
supplier functions and returns an IEnumerable<T>
of all of their results or a rejected Promise<T>
of the first exception thrown.
This method takes an IEnumerable<Task<T>
of existing Task<T>
instances or an IEnumerable<Func<Task<T>>>
of Task<T>
supplier functions and returns the result of the first to finish or a rejected Promise<T>
of the first exception thrown.
Occasionally you need to escape from the world of Promise
, either because existing code requires a raw value or Task
, or because you're starting a workflow that you want separated from the current one. In these cases, you should await
your IPromise<T>
instance. This will return a Task<T>
if the Promise
was in a fulfilled state, and will throw the exception contained in a rejected state. Generally, this is something you would do at the top level of a program, or anywhere that you need to return something to framework code (like a WebApi controller).