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

Async Binding Support #155

Merged
merged 19 commits into from
Feb 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions Documentation/Async.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@

## <a id="async-bindings"></a>Async Extensions <smal><sub>*Experimental*</sub>


## Table Of Contents

* Introduction
* <a href="#async-and-di">`async` in DI</a>
* <a href="#example">Example</a>
* Advanced
* <a href="#static-memory-pool">Static Memory Pools</a>
* <a href="#usingstatement">Using statements and dispose pattern</a>



## Introduction
### <a id="async-and-di"></a>Async in DI

In dependency injection, the injector resolves dependencies of the target class only once, often after class is first created. In other words, injection is a one time process that does not track the injected dependencies to update them later on. If a dependency is not ready at the moment of injection, either the binding wouldn't resolve in case of optional bindings or would fail completely throwing an error.

This creates a dilemma while implementing dependencies that are resolved asynchronous. You can design around the DI limitations by carefully designing your code so that the injection happens after the `async` process is completed. This requires careful planning, which leads to an increased complexity in the setup, and is also prone to errors.

Alternatively you can inject an intermediary object that tracks the result of the `async` operation. When you need to access the dependency, you can use this intermediary object to check if the `async` task is completed and get the resulting object. With the experimental `async` support, we would like to provide ways to tackle this problem in Extenject. You can find `async` extensions in the folder **Plugins/Zenject/OptionalExtras/Async**.

### <a id="example"></a>Example

Lets see how we can inject async dependencies through an intermediary object. Async extensions implements `AsyncInject<T>` as this intermediary. You can use it as follows.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Lets see how we can inject async dependencies through an intermediary object. Async extensions implements `AsyncInject<T>` as this intermediary. You can use it as follows.
Lets see how we can inject `async` dependencies through an intermediary object. `async` extensions implement `AsyncInject<T>` as this intermediary. You can use it as follows.



```csharp
public class TestInstaller : MonoInstaller<TestInstaller>
{
public override void InstallBindings()
{
Container.BindAsync<IFoo>().FromMethod(async () =>
{
await Task.Delay(100);
return (IFoo) new Foo();
}).AsCached();
}
}

public class Bar : IInitializable, IDisposable
{
readonly AsyncInject<IFoo> _asyncFoo;
IFoo _foo;
public Bar(AsyncInject<IFoo> asyncFoo)
{
_asyncFoo = asyncFoo;
}

public void Initialize()
{
if (!_asyncFoo.TryGetResult(out _foo))
{
_asyncFoo.Completed += OnFooReady;
}
}

private void OnFooReady(IFoo foo)
{
_foo = foo;
}

public void Dispose()
{
_asyncFoo.Completed -= OnFooReady;
}
}
```

Here we use `BindAsync<IFoo>().FromMethod()` to pass an `async` lambda that waits for 100 ms and then returns a newly created `Foo` instance. This method can be any other method with the signature `Task<T> Method()`. *Note: the `async` keyword is an implementation detail and thus not part of the signature. The `BindAsync<T>` extension method provides a separate binder for `async` operations. This binder is currently limited to a few `FromX()` providers. Features like Pooling and Factories are not supported at the moment.

With the above `AsyncInject<IFoo>` binding, the instance is added to the container. Since the scope is set to `AsCached()` the operation will run only once and `AsyncInject<IFoo>` will keep the result. It is important to note that `async` operations won't start before this binding is getting resolved. If you want `async` operation to start immediately after installing, use `NonLazy()` option.

Once injected to `Bar`, we can check whether the return value of the `async` operation is already available by calling the `TryGetResult`. method. This method will return `false` if there is no result to return. If result is not ready yet, we can listen to the `Completed` event to get the return value when the `async` operation completes.

Alternatively we can use the following methods to check the results availability.
```csharp
// Use HasResult to check if result exists
if (_asyncFoo.HasResult)
{
// Result will throw error if prematurely used.
var foo = _asyncFoo.Result;
}

// AsyncInject<T> provides custom awaiter
IFoo foo = await _asyncFoo;
```

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#if EXTENJECT_INCLUDE_ADDRESSABLE_BINDINGS
using System;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine.ResourceManagement.AsyncOperations;
using Zenject;

[ZenjectAllowDuringValidation]
[NoReflectionBaking]
public class AddressableInject<T> : AsyncInject<T> where T : UnityEngine.Object
{
private AsyncOperationHandle<T> _handle;
public AsyncOperationHandle AssetReferenceHandle => _handle;

public AddressableInject(InjectContext context, Func<CancellationToken, Task<AsyncOperationHandle<T>>> asyncMethod)
: base(context)
{
StartAsync(async (ct) =>
{
_handle = await asyncMethod(ct);
return _handle.Result;
}, cancellationTokenSource.Token);
}
}
#endif

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using ModestTree;

namespace Zenject
{
public static class AsyncDiContainerExtensions
{
public static
#if EXTENJECT_INCLUDE_ADDRESSABLE_BINDINGS
ConcreteAddressableIdBinderGeneric<TContract>
#else
ConcreteAsyncIdBinderGeneric<TContract>
#endif
BindAsync<TContract>(this DiContainer container)
{
return BindAsync<TContract>(container, container.StartBinding());
}

public static
#if EXTENJECT_INCLUDE_ADDRESSABLE_BINDINGS
ConcreteAddressableIdBinderGeneric<TContract>
#else
ConcreteAsyncIdBinderGeneric<TContract>
#endif
BindAsync<TContract>(this DiContainer container, BindStatement bindStatement)
{
var bindInfo = bindStatement.SpawnBindInfo();

Assert.That(!typeof(TContract).DerivesFrom<IPlaceholderFactory>(),
"You should not use Container.BindAsync for factory classes. Use Container.BindFactory instead.");

Assert.That(!bindInfo.ContractTypes.Contains(typeof(AsyncInject<TContract>)));
bindInfo.ContractTypes.Add(typeof(IAsyncInject));
bindInfo.ContractTypes.Add(typeof(AsyncInject<TContract>));

#if EXTENJECT_INCLUDE_ADDRESSABLE_BINDINGS
return new ConcreteAddressableIdBinderGeneric<TContract>(container, bindInfo, bindStatement);
#else
return new ConcreteAsyncIdBinderGeneric<TContract>(container, bindInfo, bindStatement);
#endif
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using ModestTree;

namespace Zenject
{
public interface IAsyncInject
{
bool HasResult { get; }
bool IsCancelled { get; }
bool IsFaulted { get; }
bool IsCompleted { get; }

TaskAwaiter GetAwaiter();
}


[ZenjectAllowDuringValidation]
[NoReflectionBaking]
public class AsyncInject<T> : IAsyncInject
{
protected readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
protected readonly InjectContext _context;

public event Action<T> Completed;
public event Action<AggregateException> Faulted;
public event Action Cancelled;

public bool HasResult { get; protected set; }
public bool IsSuccessful { get; protected set; }
public bool IsCancelled { get; protected set; }
public bool IsFaulted { get; protected set; }

public bool IsCompleted => IsSuccessful || IsCancelled || IsFaulted;

T _result;
Task<T> task;

protected AsyncInject(InjectContext context)
{
_context = context;
}

public AsyncInject(InjectContext context, Func<CancellationToken, Task<T>> asyncMethod)
{
_context = context;

StartAsync(asyncMethod, cancellationTokenSource.Token);
}

public void Cancel()
{
cancellationTokenSource.Cancel();
}

protected async void StartAsync(Func<CancellationToken, Task<T>> asyncMethod, CancellationToken token)
{
try
{
task = asyncMethod(token);
await task;
}
catch (AggregateException e)
{
HandleFaulted(e);
return;
}
catch (Exception e)
{
HandleFaulted(new AggregateException(e));
return;
}

if (token.IsCancellationRequested)
{
HandleCancelled();
return;
}

if (task.IsCompleted)
{
HandleCompleted(task.Result);
}else if (task.IsCanceled)
{
HandleCancelled();
}else if (task.IsFaulted)
{
HandleFaulted(task.Exception);
}
}

private void HandleCompleted(T result)
{
_result = result;
HasResult = !result.Equals(default(T));
IsSuccessful = true;
Completed?.Invoke(result);
}

private void HandleCancelled()
{
IsCancelled = true;
Cancelled?.Invoke();
}

private void HandleFaulted(AggregateException exception)
{
IsFaulted = true;
Faulted?.Invoke(exception);
}

public bool TryGetResult(out T result)
{
if (HasResult)
{
result = _result;
return true;
}
result = default;
return false;
}

public T Result
{
get
{
Assert.That(HasResult, "AsyncInject does not have a result. ");
return _result;
}
}

public TaskAwaiter<T> GetAwaiter() => task.GetAwaiter();

TaskAwaiter IAsyncInject.GetAwaiter() => task.ContinueWith(task => { }).GetAwaiter();
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#if EXTENJECT_INCLUDE_ADDRESSABLE_BINDINGS
using System;
using System.Threading.Tasks;
using UnityEngine.AddressableAssets;
using UnityEngine.Assertions;
using UnityEngine.ResourceManagement.AsyncOperations;

namespace Zenject
{
[NoReflectionBaking]
public class AddressableFromBinderGeneric<TContract, TConcrete> : AsyncFromBinderGeneric<TContract, TConcrete>
where TConcrete : TContract
{
public AddressableFromBinderGeneric(
DiContainer container, BindInfo bindInfo,
BindStatement bindStatement)
: base(container, bindInfo, bindStatement)
{}

public AsyncFromBinderBase FromAssetReferenceT<TConcreteObj>(AssetReferenceT<TConcreteObj> reference)
where TConcreteObj:UnityEngine.Object, TConcrete
{
BindInfo.RequireExplicitScope = false;

var contractType = typeof(TContract);
if (typeof(UnityEngine.Object).IsAssignableFrom(contractType))
{
var addressableInjectType = typeof(AddressableInject<>).MakeGenericType(typeof(TContract));
BindInfo.ContractTypes.Add(addressableInjectType);
}

// Don't know how it's created so can't assume here that it violates AsSingle
BindInfo.MarkAsCreationBinding = false;
SubFinalizer = new ScopableBindingFinalizer(
BindInfo,
(container, originalType) => new AddressableProviderSimple<TContract, TConcreteObj>(reference));

return this;
}

}
}
#endif

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading