Overall, the code is quite clean and easy to understand. You did a good job of subdividing the work into small and easily digestible submethods.
I did notice a few other things though, which I'll list below.
Tasks vs threads
Right now, it seems you're using Task.WhenAll and Task.Run as an async wrapper to emulate different concurrent threads. That's not what asynchronous code is for. While asynchronous code often does use multiple cores/threads, that's not a guarantee. You can run asynchronous logic on a single-core machine with a single thread, where there's no possible way of executing logic concurrently.
Do I think that you're working on a single-core machine with a single thread? No. But the point remains that you're currently conflating two different concepts that have some but not complete overlap.
I'm not saying your way of doing it doesn't work right now, but to ensure that this code works on all machines I would suggest that you use threads for this, not tasks. That way, you enforce concurrent logic instead of hoping that it works out.
Then again, if this is a small short-lived test app that will be discarded in the near future, it doesn't quite matter (but then a code review wouldn't really matter either, I guess).
I would generally expect WebApiService to have async methods, which you could be using here. However, it's possible that WebApiService doesn't have async methods and that you are unable/unwilling to implement them on WebApiService, which would be fair enough since this test application isn't trying to redevelop the service implementation but rather test the existing one.
Constructor spam
Right now you're creating a new WebApiService() 300 times (100 loops, 3 tasks each). Is that by intention, or could this be done using a single service object?
I suspect (though I cannot guarantee) you can reuse the same object without issues here, which means you could shift that instantiation outside of the loops and either pass it as a parameter or use an static variable (since everything else is static anyway).
private static WebApiService _svc = new WebApiService();
private static async Task GetLoanByLoanId(string loanNumber)
{
await Task.Run(() => {
var msg1 = _svc.GetLoanByLoanId(loanNumber);
if (string.IsNullOrWhiteSpace(msg1)) return;
Console.WriteLine($"GL {loanNumber}: {msg1.Length}");
});
}
The same goes for the other two tasks. If you instead wish to pass it as a method parameter, simply instantiate it in Main() and then thread it through your method calls all the way down to the GetLoanByLoanId method (and the other two tasks).
Since everything here is static anyway, I'd use a static class field here because it's simpler and doesn't really cause any issues in your already exclusively static code.
If return
This is quite unidiomatic:
if (string.IsNullOrWhiteSpace(msg1)) return;
Console.WriteLine($"GEFL {loanNumber}: {msg1.Length}");
// end of void method
You could simply invert the if instead of returning one line early:
if(!String.IsNullOrWhiteSpace(msg1))
Console.WriteLine($"GEFL {loanNumber}: {msg1.Length}");
This way, you don't need an eary return. Early returns have their uses when they can skip a whole lot of performance-draining steps, but when that if return is the last block of the method body, there's no futher logic (i.e. after the if) to skip.
Logging
It's weird that you're choosing to not log a message when an empty result was retrieved. You already don't quite seem to care about the received message since you're only logging its length, so I surmise that you're using these messages as for progress tracking.
I'm unsure why you're specifically not logging 0 length (or null) results. If the logging is to keep track of the progress, then those null-or-empty results are still progress and should be logged.
If you wrote this check to avoid null reference exceptions, there's better ways of avoiding those, e.g. null propagation:
Console.WriteLine($"GEFL {loanNumber}: {msg1?.Length}");
This way, you always get a message, regardless of what msg1 contains.
Reusable task logic
All three tasks have pretty much the exact same method body, except for two small differences:
private static async Task AnyTask(string loanNumber)
{
await Task.Run(() => {
var svc = new WebApiService();
var msg1 = svc.THISMETHODISDIFFERENT(loanNumber); // <- here
if (string.IsNullOrWhiteSpace(msg1)) return;
Console.WriteLine($"THISNAMEISDIFFERENT {loanNumber}: {msg1.Length}"); // <- here
});
}
But each of these methods you use always follows the same string MyMethod(string) pattern, and the log message is really just an arbitrary string value. You can therefore easily abstract this method call in a way that the rest of the task body can be reused:
private static async Task GetLoanByLoanId(string loanNumber)
{
PerformJob($"GL {loanNumber}", svc => svc.GetLoanByLoanId(loanNumber));
}
private static async Task GetLoanDataByLoanId(string loanNumber)
{
PerformJob($"GLD {loanNumber}", svc => svc.GetLoanDataByLoanId(loanNumber));
}
private static async Task GetEnvelopesForLoan(string loanNumber)
{
PerformJob($"GEFL {loanNumber}", svc => svc.GetEnvelopesForLoan(loanNumber));
}
private static async Task PerformJob(string jobName, Func<WebApiService, string> getMessage)
{
return Task.Run(() => {
var svc = new WebApiService();
var msg1 = getMessage(svc);
if (string.IsNullOrWhiteSpace(msg1)) return;
Console.WriteLine($"{jobName}: {msg1.Length}");
});
}
There are two things to remark here:
- The three job methods have become so trivial that you could arguably remove them and just call
PerformJob("xxx", svc => svc.xxx(loanNumber)); directly from the calling code. I think this is a subjective call whether you prefer to wrap it nicely or would rather avoid one liner methods.
- If you follow the improvements I suggested in earlier points, the method body of
PerformJob becomes trivial as well. But I wouldn't suggest removing this method even though it's trivial, since that would force you to copy/paste the log message format all over the place.
private static async Task PerformJob(string jobName, Func<WebApiService, string> getMessage)
{
// Verbosely:
var msg = getMessage(svc);
Console.WriteLine($"{jobName}: {msg?.Length}");
// Or as a one-liner:
Console.WriteLine($"{jobName}: {getMessage(svc)?.Length}");
}
RunLoadmethod tries to mimic several clients? Are you concerning about ramp-up time? Do I understand correctly that yourGetXYZare independent? Does ordering of these API calls matter? \$\endgroup\$