-
Notifications
You must be signed in to change notification settings - Fork 279
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
Changes from all commits
cb0b1be
7ace792
921f28c
1664704
791719c
89bcaf3
9af106d
b70860d
9de197d
3efbebe
a81a434
798721d
c64f8a1
e3114ba
b814fac
c20caac
b2708af
a88111f
df6877a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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. | ||
|
||
|
||
```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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.