Extends UnityWebRequest via async decorator pattern — Advanced Techniques of UniTask

It’s been a few months since we released UniTask v2, and we’re now at version 2.0.31, GitHub’s Star is achieved over 1100. We believe it is stable and will be used in production.

For more information on UniTask v2, see this article - UniTask v2 — Zero Allocation async/await for Unity, with Asynchronous LINQ.

With the introduction of async/await, it is possible to implement new designs. Let’s take UnityWebRequest as an example of an asynchronous, pluggable and extensible design.

I will introduce it under the name async decorator pattern, but it is commonly known as Middleware.

It is a common design, mainly on the server side, implemented as a Filter in ASP.NET Core, Middleware in node.js (Express) and React, WSGI in Python, and MagicOnion. This is a very powerful design pattern and is also useful in client side.

For example, MagicOnion(our network framework, for .NET Core and Unity) works like this.

The method is called from the outside to the inside, asynchronously.

await next(
await next(
await next()

If you want to extend UnityWebRequest, the required features are

  • Logging
  • Mocking
  • Timeout
  • Processing request header before request
  • Processing response header after request
  • Exception handling based on status code
  • UI handling after error (pop-ups, retries, scene transition)

Everything can be implemented by async decorator.

Decorator samples

The first step is to provide the following as a common interface.

Important thing is Func<RequestContext, CancellationToken, UniTask<ResponseContext>> next.

Let’s take a look at some real use cases.

As a simple example, it is handled before and after the header.

We advance inside the decorator method that is chained by await next(). So if you write before it, it’s pre-processing, if you write after it, it’s post-processing.

Now, being integrated with async/await, try-catch-finally can be written naturally as well. For example, if you prepare a logging…

It’s also easy to terminate a process, just don’t call next. For example, you can create a decorator that returns a dummy response (to be used for testing or to proceed while the server-side implementation is not ready).

Let’s also think about retry. For example, you receive a special response code that asks you to get a Token and reprocess it again.

If you want to put a queue in between to force sequential processing, you can write it like this

You can write easily from something simple to something that looks pretty complicated only provide async/await.

This is how to use the prepared decorator.

Implements async decorator

It’s a little longer, but it’s not so complicated.

The core process is InvokeRecursive. To simplify it a bit more…

Advancing IAsyncDecorator[], and “next” is the next element of the array(composed filter), and that’s the only implementation of this pattern.

Also, the NetworkClient itself is an IAsyncDecorator, which means that the one that doesn’t use next is the innermost part, the last part of the process.

A tip is that we handle the Timeout in CancellationTokenSource.CancelAfterSlim. Timeout can also be handled externally using WhenAny, but if the target has a CancellationToken argument, this is more efficient and better for terminate.

Combinate with scene transition

You know how when a network request fails, you get a pop-up that says something like, “An error has occurred, return to title, ‘OK’”? Let’s do that.

We can utilize UniTaskCompletionSource to express that we are waiting for the button to be pressed.

Now, let’s combine this with async decorator.

It’s easy to write using await, so if you have the right tools, it’s not so easy to write asynchronous processing.

One thing you need to be aware of is whether or not to return to the caller. If you return normally, the processing will go back, but if you rethrow Exception, that will come up as an error. Since returning to the title screen means that the communication process has been cancelled, it’s correct to mark the process as cancelled here; to treat it as cancelled in the async method, you need to throw an OperationCanceledException.

By utilizing async/await, you can achieve a design that is not possible with callbacks. Furthermore, with UniTask, there is no performance overhead.

We hope you will try to use UniTask as a base library for your games.

a.k.a. neuecc. Creator of UniRx, UniTask, MessagePack for C#, MagicOnion etc. Microsoft MVP for C#. CEO/CTO of Cysharp Inc. Live and work in Tokyo, Japan.