1

I've been trying to find a design for the situation where you have a type that has a dependency and you want to call a method that returns a Task. The gut reaction is to do GetAwaiter().GetResult() but I know that goes against the whole purpose of asynchronous tasks. The thought is to spin up the task but let it do its thing until the type needs it.

public class SomeClass {
    private readonly Task<Data> _getDataTask;
    private readonly IDependency _dep;

    private Data _data;

    public SomeClass(IDependency dep) {
        _dep = dep;
        // I'll spin this up but I don't need the result yet
        _getDataTask = _dep.GetDataAsync();
    }

    public async Task DoSomeWork() {
        // Now I need the result of the task so I'll await the task
        _data = await _getDataTask;
        ExecuteWorkOn(_data);
    }
}

Maybe this approach would produce a lot of condition statements to await if you don't have the result cached? I'm hoping to get feedback on this approach in the hopes that either another SO question gets linked or we come up with a design we didn't think about before.

UPDATE 1 I made the Task to be Task<Data> as mentioned in one of the comments

Logan K
  • 324
  • 2
  • 8
  • 2
    There's nothing wrong with this approach – Sean Apr 03 '20 at 13:51
  • 2
    If no one ever calls `DoSomeWork`, will the call of `GetDataAsync` have been wasted? If so, seems you could just use `Lazy` here instead to defer making the call until it's required. – Damien_The_Unbeliever Apr 03 '20 at 13:52
  • 1
    Shouldn't `_getDataTask` be `Task`? – Johnathan Barclay Apr 03 '20 at 13:58
  • @Damien_The_Unbeliever I've read on Stephen Cleary's blog about a custom built AsyncLazy type that lets you define how to get T and await it when you want it. I've thought about that too where if nobody ever calls the method that awaits the result the result is wasted. – Logan K Apr 03 '20 at 16:07
  • There's a [few different approaches](https://blog.stephencleary.com/2013/01/async-oop-2-constructors.html), depending on your needs. Or if this is a ViewModel and the async work is loading data, then [you'd need a solution like this](https://learn.microsoft.com/en-us/archive/msdn-magazine/2014/march/async-programming-patterns-for-asynchronous-mvvm-applications-data-binding). – Stephen Cleary Apr 03 '20 at 18:04
  • Your NotifyTaskCompletion example is definitely a good one. Most of what I've been having the need for is loading resources, so I think that's where AsyncLazy has a lot of use. @StephenCleary – Logan K Apr 03 '20 at 19:46

1 Answers1

0

There are two good solutions for this problem:

First:

Use a async init methode in the constructor and hold the resulting Task in a property. This way the calling code can await the completion if the initilisation.

public class Foo
{
    public Task InitTask { get; private set; }

    public Foo()
    {
        this.InitTask = this.Init();
    }

    private async Task Init() { ... }
}

can be used like this


var newFoo = new Foo();
await newFoo.InitTask();
// can now use newFoo

Second:

Use a use only private constructors and have a Create methode which you use to create intances for your class

public class Foo
{
    public Foo() { }

    public async Task<Foo> Create() 
    {
        var newFoo = new Foo();
        await newFoo.Init();
        return newFoo;
    }

    private async Task Init() { ... }
}

can be used like this

var newFoo = await Foo.Create();

Your approch, if not bad, is a variant of approch 1 but would mean that you need to await the task started in the constructor in every methode that needs the result (or side effect) of the task.

public class Foo
{
    private Task InitTask { get; private set; }

    public Foo()
    {
        this.InitTask = this.Init();
    }

    private async Task Init() { ... }

    public async Task DoStuffA() 
    {
        await this.InitTask;
        // do work
    }

    public async Task DoStuffB() 
    {
        await this.InitTask;
        // do work
    }

    public async Task DoStuffC() 
    {
        await this.InitTask;
        // do work that could be done without async/await
    }
}

So I would recommend approch 2 to do async initialization.

Ackdari
  • 3,222
  • 1
  • 16
  • 33
  • I do use the builder pattern quite a bit to build immutable types throughout my work. Maybe an async version of the builder would be in order where you `await builder.BuildAsync()`. I wonder though if an AsyncLazy may be even better where you setup how to get the data but don't get it until you do `await`. – Logan K Apr 03 '20 at 16:09
  • Re: _" I wonder though if an AsyncLazy may be even better"_ I would say that depends on how much time passes between constuction and use of the instance and wether the data are guranted to be used. – Ackdari Apr 03 '20 at 16:13