Search code examples
c#asynchronousasync-awaitentity-framework-6

Execute Bundles of Parallel Tasks in Series


I have a GUI that allows users to make changes to various objects (which embody some internal rules), then click Save to write the changes to a database using EF6. When they make a change to some objects, I want to first deactivate all the currently active objects in the database (the order is not important), and then when that is completed, I would like to insert new, active objects (again, in any order).

I thought I would be able to achieve this by making two lists of tasks - one for deactivating objects (deactivate_Tasks) and the other for adding new objects (add_Tasks). I thought that I would then be able to await Task.WhenAll(deactivate_Tasks) to make sure that the first set of tasks completed in parallel, but in series with the second set, executed on the next line (await Task.WhenAll(add_Tasks)). Thus the additions would only occur once the deactivations have completed.

However, when I run the code, I get seemingly erratic results, with all the adding and deactivating tasks occurring in an unpredictable order. I don't know why this is and I would like to avoid it - any suggestions would be really welcome.

I have mocked up a sample of my code below to help - hopefully the method names are self-explanatory enough, but please ask if not. In the real case, there are more types of rule to be added and deactivated. I am using C# 4.8 and writing a windows WPF application using the MVVM framework as much as I can.

public class MyViewModel
{
    private ICommand _saveCommand;
    public ICommand SaveCommand
    {
        get
        {
            return _saveCommand ?? (_saveCommand = new RelayCommand(execute => Save(), canExecute => IsDirty));
        }
        set
        { _saveCommand = value; }
    }
    private RuleDataService _ruleDataService { get; set; }

    public async void Save()
    {
        var add_Tasks = new List<Task<StatusCode>>();
        var deactivate_Tasks = new List<Task<StatusCode>>();

        if (CustomerRule_IsDirty)
        {
            add_Tasks.Add(_rulesDataService.CREATE_Rule(newCustomerRule));
            if (existingCustomerRule != null)
            {
                deactivate_Tasks.Add(_rulesDataService.DEACTIVATE_Rule(existingCustomerRule));
            }
        }
        if (WarehouseRule_IsDirty)
        {
            add_Tasks.Add(_rulesDataService.CREATE_Rule(newWarehouseRule));
            if (existingWarehouseRule != null)
            {
                deactivate_Tasks.Add(_rulesDataService.DEACTIVATE_Rule(existingWarehouseRule))
            }
        }

//MY COMMENTS REFLECT WHAT I HOPED TO ACHIEVE, NOT WHAT ACTUALLY HAPPENS
        //wait for all the deactivations to be done
        await Task.WhenAll(deactivate_Tasks).ConfigureAwait(false);
        //once everything is deactivated, add the replacements
        await Task.WhenAll(add_Tasks).ConfigureAwait(false);
    }
}

/// <summary>
/// Sample only - hopefully the methods names used above are self explanatory, but they all return a Task<StatusCode>
/// </summary>
public class RuleDataService
{
    public async Task<StatusCode> DEACTIVATE_Rule(Customer_Rule customer_Rule)
    {
        using(var context = new DBContext())
        {
            context.Database.Log = s => System.Diagnostics.Debug.WriteLine(s);
            return await Task.Run(() =>
            {
                foreach (var existingRule in context.Customer_Rules.Where(r => r.CustomerName == customer_Rule.CustomerName && r.IsActive && r.RuleSetId != customer_Rule.RuleSetId))
                {
                    existingRule.IsActive = false;
                }
                context.SaveChanges();
                return StatusCode.TaskComplete;

            }).ConfigureAwait(false);
        }
    }

    public async Task<StatusCode> DEACTIVATE_Rule(Warehouse_Rule warehouse_Rule)
    {
        //basically the same as the other DEACTIVATE methods, just a different table
    }

    public async Task<StatusCode> CREATE_Rule(Customer_Rule customer_Rule)
    {
        //basically the same as the other DB methods, but performs an Add instead of an UPDATE
    }

    public async Task<StatusCode> CREATE_Rule(Warehouse_Rule warehouse_Rule)
    {
        //basically the same as the other DB methods, but performs an Add instead of an UPDATE
    }
}

I have done a fair amount of googling for answers, but the answers only seem to give advice on how to run a series of tasks in parallel, which I have achieved, not how to bundle up in-series sets of parallel actions.


Solution

  • await is akin to "continue execution when this task is done". Notably, it does not say anything at all about when the task is started. So your example code both add and deactivate tasks will run in parallel, resulting in the behavior you describe. The solution is to start the add tasks after the deactivate tasks. I.e.

    // Create all deactivate tasks
    if (CustomerRule_IsDirty && existingCustomerRule != null)
    {
        deactivate_Tasks.Add(_rulesDataService.DEACTIVATE_Rule(existingCustomerRule));
    }
    if (WarehouseRule_IsDirty && existingWarehouseRule  != null)
    {
        deactivate_Tasks.Add(_rulesDataService.DEACTIVATE_Rule(existingWarehouseRule))
    }
    
    await Task.WhenAll(deactivate_Tasks).ConfigureAwait(false);
    
    // Create all add tasks
    if (CustomerRule_IsDirty)
    {
         add_Tasks.Add(_rulesDataService.CREATE_Rule(newCustomerRule));
    }
    if (WarehouseRule_IsDirty)
    {
         add_Tasks.Add(_rulesDataService.CREATE_Rule(newWarehouseRule));
    }
    await Task.WhenAll(add_Tasks).ConfigureAwait(false);
    

    also, you are creating the DbContext object in another thread-context than where it is used. I would suggest moving the creation and disposal into the Task.Run(...) just to avoid any potential issues.