Scenario
In an MVVMLight based app I use a couple of views and services to do some work time calculation. Among others there are the following services...
- DataService: As the name suggests, used to get the data from the database. Because it's I/O operations per nature the methods are using async/await.
- Contract: This is a CPU bound service that actually calculates the work times (e.g. for a given day, week, month, ...)
Now, let's have a look at the following method (which is located in the Contract service):
public TimeSpan GetWorkTimeForDay(Date date)
{
// Check if that result was already calculated before and is still
// valid. In this case, load it and return it
if (ContractCalculationCacheResults.Contains(date))
return ContractCalculationCacheResults.Get(date);
var finalWorkTime = TimeSpan.Zero;
// Get the work day from the database in a synchronous fashion
var workDay = DataService.GetWorkDay(date);
// if there is a work day in the database then finally calculate the work time
if (workDay != null)
finalWorkTime = GetWorkTimeForDay(workDay);
// cache the result
ContractCalculationCacheResults.Put(date, finalWorkTime);
// return the final result
return finalWorkTime;
}
This method is located in the Contract
service. It has the dependency to the DataService
to get the required data for the calculations.
As you can see, DataService.GetWorkDay()
is synchronous at the moment. This is how the database got accessed for quite some time. Now, while porting the app from WP8 Silverlight to UWP I wanted to clean up the code and changed the data service so that it only provides async
methods instead of synchronous methods. But now I'm struggling with combining two actually separate services where ones nature is synchronous while the others is asynchronous.
Problem
I need to update the method GetWorkTimeForDay()
(and many others that look similar) to use the new DataService.GetWorkDayAsync()
method which, as the name applies, is defined as follows:
public async Task<WorkDay> GetWorkDayAsync(Date dateKey)
However, this leads to many open questions as suddenly GetWorkTimeForDay
should become an async
method as well, even though its nature is CPU bound. So this won't make sense.
Possible Solutions
1. Make GetWorkTimeForDay
an async method
The result would be something like this:
public async Task<TimeSpan> GetWorkTimeForDay(Date date)
{
...
// Get the work day from the database in a synchronous fashion
var workDay = await DataService.GetWorkDayAsync(date);
...
}
While this looks easy, it has a lot of implications on the rest of the code. Simply because that everywhere where GetWorkTimeForDay
is called I would have to change the code because this method is now async
. I would have to await
for it and therefore might need to change the calling method as well... and so on. But sometimes it is not required to call GetWorkTimeForDay()
asynchronously simply because it is already running in a separate thread or a background task. Let alone all the Unit-Test that exist so far. So there are cases where it won't block the UI.
Furthermore, suddenly the CPU bound calculations become asynchronous which is not how it should work (at least if I understood this post (and others) by Stephen Cleary correctly.
2. Separate the services
So the next solution that came to my mind is to separate the services. In other words, do not have one service depend on another. This would change the signature for GetWorkTimeForDay()
like so:
public TimeSpan GetWorkTimeForDay(WorkDay workDay, Date date)
{
var finalWorkTime = TimeSpan.Zero;
if (workDay != null)
finalWorkTime = GetWorkTimeForDay(workDay);
return finalWorkTime;
}
The calling method would then look similar to this:
public async void Caller(Date date)
{
...
if (ContractCalculationCacheResults.Contains(date))
return ContractCalculationCacheResults.Get(date);
var workDay = await DataService.GetWorkDay(date);
var workTime = Contract.GetWorkTimeForDay(workDay, date);
ContractCalculationCacheResults.Put(date, workTime);
...
}
The downside, just like for for the other solution, is that all the caller methods would have to be updated (and its callers, ...) even though sometimes this is not necessary. Also a lot of redundant code for the caching and database access will be added.
One more downside, from an MVVM point of view, is probably that suddenly the data is provided to the contract service. So I won't use DI anymore which is more or less against the rules for good reasons. (Here to me the question pops up on how to correctly inject a service into a service)
Last but not least, I would have to remove the caching from the GetWorkTimeForDay
method as well. Only when the result was not cached before, I'm gonna ask the database to get the work day and do the calculations afterwards.
3. ?
...
Actual Question
What is a good design to combine an async
service together with a non-async
service in an MVVM based project?
I want to keep the actual calculations synchronous because they are CPU bound. I want to keep the data access asynchronous because they are not CPU bound but might take some time.
So this post is not about a technical solution on how to wait for the result of await
/Task/... . It is more an architectural question about MVVM, services injected into other services, combination of services that are synchronous and asynchronous.