Home > Enterprise >  How to adapt Task<T> to provide dependency inversion principle without loss of async/await syn
How to adapt Task<T> to provide dependency inversion principle without loss of async/await syn

Time:11-03

There are plenty of classes in .NET standard library that have no interfaces. It ignores the dependency inversion principle. There is the same story with static methods. But we can fluently adapt them like

class DateTimeProvider : IDateTimeProvider {
   public DateTime GetNow() => DateTime.Now;
}

From time to time, it is necessary to make this adaptation, especially for unit testing. I found one more reason to adapt the class to be interfaced. It is covariance. Missing covariance is painful, and the most painful example is Task<T>.The covariance type parameter can't be declared in the generic class. And the following doesn't work.

class A {}
class B : A {}
...
Task<A> a = GetBAsync();

Hmm, it looks like I need to adapt it to make it has an additional interface. But it isn't so easy as with DateTime. The extra point is that C# has async/await syntax construction that depends on Task. I don't want to lose this construction.

After some investigation, I found out that it is possible to do it by implementing some interfaces. Some of those interfaces are extensional (some specific methods/properties that are necessary to be implemented but not included in any C# interface).

So, I have declared the following interfaces (to have covariance) and implemented the following classes.

interface IAwaiter<out T> : ICriticalNotifyCompletion
{
    bool IsCompleted { get; }

    T GetResult();
}
struct Awaiter<T> : IAwaiter<T>
{
    private readonly TaskAwaiter<T> _origin;

    public Awaiter(TaskAwaiter<T> origin) =>
        _origin = origin;

    public bool IsCompleted =>
        _origin.IsCompleted;

    public T GetResult() =>
        _origin.GetResult();

    public void OnCompleted(Action continuation) =>
        _origin.OnCompleted(continuation);

    public void UnsafeOnCompleted(Action continuation) =>
        _origin.UnsafeOnCompleted(continuation);
}
interface IAsyncJob<out T>
{
    IAwaiter<T> GetAwaiter();
}
struct Job<T> : IAsyncJob<T>
{
    private readonly Task<T> _task;

    public Job(Task<T> task) =>
         _task = task;

    public IAwaiter<T> GetAwaiter() => 
         new Awaiter<T>(_task.GetAwaiter());
}

After that await started to work with my custom type.

class A {}
class B : A {}
...
IAsyncJob<B> bJob = new Job<B>(Task.FromResult(new B()));
IAsyncJob<A> aJob = bJob;
A = await a;

Great! But the problem with async still exists. I can't use my interface IAsyncJob<T> in the following context:

async IAsyncJob<B> GetBAsync() { ... }

I investigated it deeper and found out that we can solve the problem by implementing the extensional interface and attaching it to my task-like type with attribute. After the following changes, it started to be compilable.

public class JobBuilder<T>
{
    public static JobBuilder<T> Create() => null;

    public void Start<TStateMachine>(ref TStateMachine stateMachine)
        where TStateMachine : IAsyncStateMachine
    { }

    public void SetStateMachine(IAsyncStateMachine stateMachine) { }

    public void SetResult(T result) { }

    public void SetException(Exception exception) { }

    public IAsyncJob<T> Task => default(IAsyncJob<T>);

    public void AwaitOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
            where TAwaiter : INotifyCompletion
            where TStateMachine : IAsyncStateMachine
    { }

    public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
            where TAwaiter : ICriticalNotifyCompletion
            where TStateMachine : IAsyncStateMachine
    { }
}
[AsyncMethodBuilder(typeof(JobBuilder<>))]
public interface IAsyncJob<out T>
{
    IAsyncJobAwaiter<T> GetAwaiter();
}

Obviously, my JobBuilder<T> is a stub and I need some implementation that will make my code not only compilable but workable. It should work the same as the default behavior for Task<T>.

Maybe, there is some Builder implementation that is used for Task<T> by default and I can delegate calls to it (I didn't find this implementation).

How to implement JobBuilder<T> to have the same for IAsyncJob<T> as .Net provides for Task<T>?


Note: Of course, I have covariance when I do 'unboxing' of Task<T> with 'await'. And the following works fine:

A a = await GetBAsync();
IEnumerable<A> aCollection = await GetBCollectionAsync();

Also, it isn't a problem to do any conversions including casting at the execution level.

static async Task<TOut> Map<TIn, TOut>(
    this Task<TIn> source,
    Func<TIn, TOut> f) => 
        Task.FromResult(f(await source));
...
var someNumber = await Task
    .Run(() => 42)
    .Map(x => (double)x)
    .Map(x => x * 2.2);

But it is nothing when I want to have covariance in type system level (inside another interface that uses Task<T> in outputs).

The following is still covariant

public interface IJobAsyncGetter<out T>
{
    IAsyncJob<T> GetAsync();
}

but

public interface ITaskAsyncGetter<out T>
{
    Task<T> GetAsync();
}

is not.

It excludes covariants from solution design abilities.

And

IEnumerable<IJobAsyncObject<B>> b = ...
IEnumerable<IJobAsyncObject<B>> a = a;

works, but

IEnumerable<ITaskAsyncObject<B>> b = ...
IEnumerable<ITaskAsyncObject<B>> a = a;

doesn't.

It looks like Task<T> is one of .NET mistakes that lives with us because of backward compatibility. I see

public interface IAsyncEnumerable<out T>

is covariant and awaitable interface that works.

I am sure there was possible to provide ITask<T> in the same way. But it has not been. So, I am trying to adapt it. It isn't a local code problem for a quick solution. I am implementing a monadic async tree, and it is a core part of my framework. I need true covariance. And missing of it blocks me.

CodePudding user response:

Frankly I don't know how to implement what you are looking for. However, there is a easy workaround with minimal syntax. The idea is you await GetBAsync convert B to A, wrap A in a task and then return it. It is a extra-step, but a simple one:

Task<A> a = Task.Run(async () => (A)await b.GetBAsync());

That way you fully keep the async/await functionality.

CodePudding user response:

Instead of going through all this, I would recommend writing simple wrapper function for that specific problem:

// Sample classes
class Parent { }
class Child : Parent { }

public class Program
{
    static async Task Main(string[] args)
    {
        Task<Parent> taskToGetChild = As<Child, Parent>(GetChildAsync);
        // some code
        var b = await taskToGetChild;
    }

    static async Task<Child> GetChildAsync()
    {
        return await Task.FromResult(new Child());
    }

    // Here's the method that allow you to mimic covariance.
    static async Task<TOut> As<TIn, TOut>(Func<Task<TIn>> func)
        where TIn : TOut
    {
        return (TOut)await func();
    }
}

CodePudding user response:

You can still adapt Task<T> to a degree without having to implement a custom task-like type. IAsyncEnumerator<T> can give you a bit hint.

I'd define an interface like this:

interface IAsync<out T>
{
    T Result { get; }
    
    Task WaitAsync();
}

Implement the interface with a wrapper of Task<T>:

public class TaskWrapper<T> : IAsync<T>
{
    private readonly Task<T> _task;

    public TaskWrapper(Task<T> task)
    {
        _task = task;
    }

    public T Result => _task.Result;

    public async Task WaitAsync()
    {
        await _task;
    }
}

Add an extension method to complete the async action and return a Task of result:

public static class AsyncExtensions
{
    public static async Task<T> GetResultAsync<T>(this IAsync<T> task)
    {
        await task.WaitAsync();
        return task.Result;
    }
}

With the above, you could do in your code:

class A { }
class B : A { }

IAsync<B> b = new TaskWrapper<B>(Task.FromResult<B>(new B()));
IAsync<A> a = b;

IEnumerable<IAsync<B>> bList = new List<IAsync<B>>();
IEnumerable<IAsync<A>> aList = bList;

A aValue = await a.GetResultAsync();
  • Related