✅ run async funtion from constructor

we can't use await keyword in the constructor because we can't add async keyword to it. How to run a function that returns a Task asynchronously then from the constructor? Or do we have to run it synchronously in that case? I remember vaguely that there was an old legacy way in .Net to run Tasks asynchronously without the await keyword. I believe it was ContinueWith(), is that what we wanna do in that case? If we do run it synchronously instead, what is the best way to run a Task synchronously out of the many possible existing ways? There is Task.Result, Task.Wait() Task.GetAwaiter().GetResult(), Task.RunSynchronously() and probably 20 more ways I don't know yet xD which one is generally prefered?
36 Replies
Tvde1
Tvde12y ago
Short answer: you can't wait for an async task inside a constructor what you can do, it make the constructor private, and add a static build/create method Instead of:
class Person
{
public string Name { get; }
public Person()
{
Name = await Database.Sql("..."); // illegal
}
}
class Person
{
public string Name { get; }
public Person()
{
Name = await Database.Sql("..."); // illegal
}
}
Do something like:
class Person
{
public string Name { get; }
private Person(string name)
{
Name = name;
}

public static Task<Person> BuildAsync()
{
var myName = await Database.Sql("...");
return new Person(myName);
}
}
class Person
{
public string Name { get; }
private Person(string name)
{
Name = name;
}

public static Task<Person> BuildAsync()
{
var myName = await Database.Sql("...");
return new Person(myName);
}
}
Florian Voß
Florian VoßOP2y ago
thats a nice workaround but I'd like to keep my constructor 😦 public
canton7
canton72y ago
So, you want the constructor to return before the operation has finished, or after?
Tvde1
Tvde12y ago
it's a really really really good practice, that after the constructor is ran, the object is valid. So avoid
var user = new User();
await user.Initialize(someData);
var user = new User();
await user.Initialize(someData);
Because you will forget or call it multiple times
Florian Voß
Florian VoßOP2y ago
ideally after
canton7
canton72y ago
Then your constructor needs to block until the operation has finished, which is really really bad practice Calling a constructor should never be expensive (and it should never fail in inconsistent ways, in the way that a database query might)
Florian Voß
Florian VoßOP2y ago
I don't think it needs to be blocking tho. Can't we use ContinueWith() instead of await keyword to run the Task asynchronously without blocking?
canton7
canton72y ago
But then your constructor will return before the operation is complete Which is why I asked that question 🙂
Tvde1
Tvde12y ago
ContinueWith will chain the task with a next task that will continue after one is done, it's a different use case
canton7
canton72y ago
You can do exactly the same by calling an async void method from your constructor (without the pain of ContinueWith)
Tvde1
Tvde12y ago
e.g.
getGroceriesTask.ContinueWith(groceries => UnloadIntoFridge())
Console.WriteLin("I've started getting groceries. Will be back soon!");
getGroceriesTask.ContinueWith(groceries => UnloadIntoFridge())
Console.WriteLin("I've started getting groceries. Will be back soon!");
Florian Voß
Florian VoßOP2y ago
are you sure about this? My understanding is that ContinueWith waits for the Task to finish without blocking just like await does and then runs the continuation just like await would
Tvde1
Tvde12y ago
kinda and it returns the new task that will wait for both to complete
canton7
canton72y ago
I'm very very sure that ContinueWith returns before the Task being awaited completes
Florian Voß
Florian VoßOP2y ago
doesn't seem like it tho
Tvde1
Tvde12y ago
but you can't:
public User()
{
someTask.ContinueWith(() => ... the constructor?
}
public User()
{
someTask.ContinueWith(() => ... the constructor?
}
canton7
canton72y ago
Which means that your constructor returns before the operation has completed
Florian Voß
Florian VoßOP2y ago
ohh right the continuation I'd like to run is the rest of constructor which isnt possible lol 😄 mhm
canton7
canton72y ago
I mean, you can put some of the ctor code into the continuation... But your actual constructor is going to return before that code is run That, after all, is the whole freaking point of tasks and async
Florian Voß
Florian VoßOP2y ago
@canton7 you suggest same workaround like this as @Tvde1 suggested?
canton7
canton72y ago
The correct thing to do is use a factory method It's not a "workaround" Constructors must be cheap, and they must not throw because of things unrelated to their input A constructor which waits for a DB call, and might randomly fail because of e.g. network stuff, is very bad The correct thing to do is to fetch the data first, and then when you've got the data, construct the object from the data And that's what the factory method does
Florian Voß
Florian VoßOP2y ago
true, I understand. Thank you! thank you too @Tvde1
Tvde1
Tvde12y ago
No problem :)
canton7
canton72y ago
👍
Florian Voß
Florian VoßOP2y ago
even tho I won't go with running synchronously / potentially blocking, could you still answer the last part of my question pls guys? 🙂
what is the best way to run a Task synchronously out of the many possible existing ways? There is Task.Result, Task.Wait() Task.GetAwaiter().GetResult(), Task.RunSynchronously() and probably 20 more ways I don't know yet xD which one is generally prefered?
They all seem to be doing the same thing to me, They run it synchronously / potentially blocking. Does it matter which one to use?
canton7
canton72y ago
Task.GetAwaiter().GetResult() is best. Task.Wait() and Task.Result do the same thing (Task.Result is only available on Task<T>), but the gotcha is that if an exception happens, both Task.Wait() and Task.Result will wrap it in an AggregateException, which is annoying. Task.GetAwaiter().GetResult() doesn't do this, and just throws the original exception Of course, it can still deadlock, so ideally don't call it at all Task.RunSynchronously is something different. I don't think there are any other ways of doing it?
Florian Voß
Florian VoßOP2y ago
awesome, thank you I'm aware 🙂 I see, I thought based of the name it would be similar to the other things
public class M42HttpClient
{
private readonly AccessTokenDTO _accessToken;
private M42HttpClient(HttpClient httpClient, AccessTokenDTO accessToken) {
_client = httpClient;
_accessToken = accessToken;
}

public static async Task<M42HttpClient> CreateAsync(HttpClient httpClient)
{
var accessToken = await GetAccessTokenAsync();
return new M42HttpClient(httpClient , accessToken);
}
}
public class M42HttpClient
{
private readonly AccessTokenDTO _accessToken;
private M42HttpClient(HttpClient httpClient, AccessTokenDTO accessToken) {
_client = httpClient;
_accessToken = accessToken;
}

public static async Task<M42HttpClient> CreateAsync(HttpClient httpClient)
{
var accessToken = await GetAccessTokenAsync();
return new M42HttpClient(httpClient , accessToken);
}
}
The issue now is, how is my DI / IoC Container supposed to find the constructor now to inject the dependencies? Can I have it invoke CreateAsync instead of ctor somehow? @canton7 @Tvde1
Tvde1
Tvde12y ago
I think I would get/re-use the access token on every request I do e.g. how long is the access token valid for
canton7
canton72y ago
An here we go down the DI rabbit hole 😄 The answer with any DI problem like this is "more factories"
Tvde1
Tvde12y ago
so
public class M42HttpClient
{
public Task<Something> MakeSomeCall()
{
var accessToken = await GetOrCreateAccessToken();
return await _httpClient.Get(..., accesToken);
}
}
public class M42HttpClient
{
public Task<Something> MakeSomeCall()
{
var accessToken = await GetOrCreateAccessToken();
return await _httpClient.Get(..., accesToken);
}
}
where GetOrCreateAccessToken remembers the one it already has and checks whether it's still valid I would probably DI an IAccessTokenProvider which may be a singleton or something :)
canton7
canton72y ago
public class M42HttpClient
{
private readonly AccessTokenDTO _accessToken;
public M42HttpClient(HttpClient httpClient, AccessTokenDTO accessToken) {
_client = httpClient;
_accessToken = accessToken;
}
}

public class M42HttpClientFactory
{
private readonly HttpClient _httpClient;
public M42HttpClientFactory(HttpClient httpClient) => _httpClient = httpClient;

public async Task<M42HttpClient> CreateAsync()
{
var accessToken = await GetAccessTokenAsync();
return new M42HttpClient(_httpClient, accessToken);
}
}
public class M42HttpClient
{
private readonly AccessTokenDTO _accessToken;
public M42HttpClient(HttpClient httpClient, AccessTokenDTO accessToken) {
_client = httpClient;
_accessToken = accessToken;
}
}

public class M42HttpClientFactory
{
private readonly HttpClient _httpClient;
public M42HttpClientFactory(HttpClient httpClient) => _httpClient = httpClient;

public async Task<M42HttpClient> CreateAsync()
{
var accessToken = await GetAccessTokenAsync();
return new M42HttpClient(_httpClient, accessToken);
}
}
(If you just want to take what you've got, although I'd listen to @Tvde1)
Florian Voß
Florian VoßOP2y ago
To check whether the Token is still valid: the AccessToken I receive from the Api has a "LifeTime" field. For checking if its still valid, I'd have to sum up DateTime.Now (when token was received) + it's lifetime and check if that is less than or equal to DateTime.Now right? if so we can safely reuse token, otherwise we generate new token?
Tvde1
Tvde12y ago
yeah something like that
Omnissiah
Omnissiah2y ago
still better than a constructor that could crash
canton7
canton72y ago
I didn't say that was a bad thing 😃
Florian Voß
Florian VoßOP2y ago
/closed
Want results from more Discord servers?
Add your server