1

I've been working on this problem for a week now and I'm getting so frustrated with EF. First off I have a super table -> sub table pattern going on in the database. It was designed with a code-first approach. The super type is called the WorkflowTask and is defined as follows:

<!-- language: c# -->
public abstract class WorkflowTask 
{
    public int WorkflowTaskId { get; set; }
    public int Order { get; set; }        
    public WorkflowTaskType WorkflowTaskType { get; set; }
    public WorkFlowTaskState State { get; set; }
    public ParentTask ParentTask { get; set; }        
    public WorkflowDefinition WorkflowDefinition { get; set; }
}

An example sub task would inherit from this task and provide additional properties:

<!-- language: c# -->
public class DelayTask : WorkflowTask
{
    public int Duration { get; set; }
}

This is mapped to the database as follows:

<!-- language: c# -->
public class WorkflowTaskEntityConfiguration : EntityTypeConfiguration<WorkflowTask>
{
    public WorkflowTaskEntityConfiguration()
    {

        HasKey(w => w.WorkflowTaskId);
        Property(w => w.WorkflowTaskId).HasColumnName("Id");
        Property(w => w.Order).HasColumnName("Order");
        Property(w => w.WorkflowTaskType).HasColumnName("TaskTypeId");
        Property(w => w.State).HasColumnName("TaskStateId");
        HasOptional(c => c.ParentTask).WithMany()
                .Map(c => c.MapKey("ParentTaskId"));
    }
}

The delay task is mapped as follows:

<!-- language: c# -->
public class DelayTaskEntityConfiguration : EntityTypeConfiguration<DelayTask>
{
    public DelayTaskEntityConfiguration()
    {
        Property(d => d.WorkflowTaskId).HasColumnName("DelayTaskId");
        Property(d => d.Duration).HasColumnName("Duration");    
    }
} 

Hopefully you get the idea. Now I have another sub type called a container task. This task will hold other tasks and can potentially hold other container tasks. Here is what it looks like as well as the mapping:

<!-- language: c# -->
public class ContainerTask : ParentTask
{
    public ContainerTask()
    {
        base.WorkflowTaskType = WorkflowTaskType.Container;
        base.ParentTaskType = ParentTaskType.ContainerTask;
    }        
    public List<WorkflowTask> ChildTasks { get; set; }
}

public class ContainerTaskEntityConfiguration : EntityTypeConfiguration<ContainerTask>
{
    public ContainerTaskEntityConfiguration()
    {            

        Property(x => x.WorkflowTaskId).HasColumnName("ContainerTaskId");           
        HasMany(c => c.ChildTasks).WithMany()
            .Map(c => c.ToTable("ContainerTaskChildren", WorkflowContext.SCHEMA_NAME)
                       .MapLeftKey("ContainerTaskId")
                       .MapRightKey("ChildTaskId"));                  
    }
}

And to make sure I include everything; here is the ParentTask object as well as it's mapping:

<!-- language: c# -->
public abstract class ParentTask : WorkflowTask
{
    public ParentTaskType ParentTaskType {get; set;}
}

public class ParentTaskEntityConfiguration : EntityTypeConfiguration<ParentTask>
{
    public ParentTaskEntityConfiguration()
    {
        Property(w => w.WorkflowTaskId).HasColumnName("ParentTaskId");
        Property(w => w.ParentTaskType).HasColumnName("ParentTaskTypeId");
    }
}

Now the item I'm trying to save is the WorkflowDefinition object. It will execute a bunch of tasks in order. It is defined as follows:

<!-- language: c# -->
public class WorkflowDefinition 
{
    public int WorkflowDefinitionId { get; set; }
    public string WorkflowName { get; set; }
    public bool Enabled { get; set; }

    public List<WorkflowTask> WorkflowTasks { get; set; }
}

public class WorkflowDefinitionEntityConfiguration :
                                   EntityTypeConfiguration<WorkflowDefinition>
{
    public WorkflowDefinitionEntityConfiguration()
    {
        Property(w => w.WorkflowDefinitionId).HasColumnName("Id");
        HasMany(w => w.WorkflowTasks)
            .WithRequired(t=> t.WorkflowDefinition)               
            .Map(c => c.MapKey("WorkflowDefinitionId"));

        Property(w => w.Enabled).HasColumnName("Enabled");
        Property(w => w.WorkflowName).HasColumnName("WorkflowName");
    }
}

So with all that defined I am passing a WorkflowDefinition object into my data repository layer and want to save it using EF. Since the object has lost it's context while working on it in the UI; I have to re-associate it so that it knows what to save. Within the UI I can add new tasks to the workflow, edit existing tasks as well as delete tasks. If there was only one level of tasks (definition => tasks) this would be cake. My problem lies with there being the possibility of infinite levels (definition => tasks => childtasks => childtasks, etc...).

Currently I retrieve the existing workflow from the database and assign the values over (workflow is the value being passed in and is of type WorkflowDefinition):

<!-- language: c# -->
// retrieve the workflow definition from the database so that it's within our context
var dbWorkflow = context.WorkflowDefinitions
                    .Where(w => w.WorkflowDefinitionId ==workflow.WorkflowDefinitionId)
                    .Include(c => c.WorkflowTasks).Single();

// transfer the values of the definition to the one we retrieved.
context.Entry(dbWorkflow).CurrentValues.SetValues(workflow);

I then loop through the list of tasks and either add them to the definition or find them and set their values. I added a function to the WorkflowTask object called SetDefinition which sets the WorkflowDefinition to the workflow within the context (previously I would get a key error because it thought the parent workflow was a different one even though the Ids matched). If it`s a container I run a recursive function to try and add all the children to the context.

<!-- language: c# -->
foreach (var task in workflow.WorkflowTasks)
{
    task.SetDefinition(dbWorkflow);

    if (task.WorkflowTaskId == 0)
    {
        dbWorkflow.WorkflowTasks.Add(task);
    }
    else
    {
        WorkflowTask original = null;
        if (task is ContainerTask)
        {
            original = context.ContainerTasks.Include("ChildTasks")
                                .Where(w => w.WorkflowTaskId == task.WorkflowTaskId)
                                .FirstOrDefault();
            var container = task as ContainerTask;
            var originalContainer = original as ContainerTask;
            AddChildTasks(container, dbWorkflow, context, originalContainer);
        }
        else
        {
            original = dbWorkflow.WorkflowTasks.Find(t => t.WorkflowTaskId == 
                                                             task.WorkflowTaskId);
        }
        context.Entry(original).CurrentValues.SetValues(task);
    }
}

The AddChildTasks function looks like this:

<!-- language: c# -->
private void AddChildTasks(ContainerTask container, WorkflowDefinition workflow, 
                           WorkflowContext context, ContainerTask original)
    {
        if (container.ChildTasks == null) return;

        foreach (var task in container.ChildTasks)
        {
            if (task is ContainerTask)
            {
                var subContainer = task as ContainerTask;
                AddChildTasks(subContainer, workflow, context, container);
            }

            if (task.WorkflowTaskId == 0)
            {
                if (container.ChildTasks == null) 
                    container.ChildTasks = new List<WorkflowTask>();
                original.ChildTasks.Add(task);
            }
            else
            {
                var originalChild = original.ChildTasks
                       .Find(t => t.WorkflowTaskId == task.WorkflowTaskId);
                context.Entry(originalChild).CurrentValues.SetValues(task);
            }
        }
    }

To delete tasks Ive found Ive had to do a two step process. Step 1 involves going through the original definition and marking tasks that are no longer in the passed in definition for deletion. Step 2 is simply setting the state for those tasks as deleted.

<!-- language: c# -->
var deletedTasks = new List<WorkflowTask>();
foreach (var task in dbWorkflow.WorkflowTasks)
{
    if (workflow.WorkflowTasks.Where(t => t.WorkflowTaskId == 
                  task.WorkflowTaskId).FirstOrDefault() == null)
        deletedTasks.Add(task);
}

foreach (var task in deletedTasks)
    context.Entry(task).State = EntityState.Deleted;

Here is where I run into problems. If I delete a container I get a constraint error because the container contains children. The UI holds all changes in memory until I hit save so even if I deleted the children first it still throws the constraint error. I`m thinking I need to map the children differently, maybe with a cascade delete or something. Also, when I loop through the tasks in the delete loop, both the container and child get flagged for deletion when I only expect the container to be flagged and the child to be deleted as a result.

Finally, the save portion above took me a good week to figure out and it looks complicated as hell. Is there an easier way to do this? I'm pretty new to EF and I'm starting to think it would be easier to have the code generate SQL statements and run those in the order I want.

This is my first question here so I apologize for the length as well as the formatting... hopefully it's legible :-)

Zoey
  • 226
  • 2
  • 13

2 Answers2

0

One suggestion, and I have not used it in the situation where there is this endless possible recursion, but if you want the cascade on delete to work natively and it looks like all tasks belong directly owned by some parent task you could approach it by defining an Identifying Relationship:

modelBuilder.Entity<WorkFlowTask>().HasKey(c => new {c.WorkflowTaskID, 
                    c.ParentTask.WofkflowTaskId});

This question is relevant: Can EF automatically delete data that is orphaned, where the parent is not deleted?

Edit: And this link: https://stackoverflow.com/a/4925040/1803682

Community
  • 1
  • 1
Matthew
  • 9,851
  • 4
  • 46
  • 77
  • I knew I would forget some information. Ok, the WorkflowDefinition will have a level 0 list of tasks, basically any task that has no parent is considered at the root of the WorkflowDefinition. After that only tasks inside a container task will have a parent. There is another task that has children but they are handled a different way and I haven't even gotten to that yet. I checked out the links provided but I think my brain is fried with too much EF info for today. I'll see what I can figure out tomorrow... thanks for taking the time. – Zoey Sep 09 '13 at 20:51
0

Ok, it took me way longer than I had hoped but I think I finally figured it out. At least all my tests are passing and all the definitions I've been saving have been working so far. There might be some combinations that I haven't thought about but for now I can at least move on to something else.

First off, I switched passing in my object as a tree and decided to flatten it out. This just means that all tasks are visible from the root but I still need to set parent properties as well as child properties.

foreach (var task in workflow.WorkflowTasks)
    {
    taskIds.Add(task.WorkflowTaskId);  //adding the ids of all tasks to use later
    task.SetDefinition(dbWorkflow);    //sets the definition to record in context
    SetParent(context, task);          //Attempt to set the parent for any task

    if (task.WorkflowTaskId == 0)
    {
        // I found if I added a task as a child it would duplicate if I added it
        // here as well so I only add tasks with no parents
        if (task.ParentTask == null)
            dbWorkflow.WorkflowTasks.Add(task);
    }
    else
    {
        var dbTask = dbWorkflow.WorkflowTasks.Find(t => t.WorkflowTaskId == task.WorkflowTaskId);
        context.Entry(dbTask).CurrentValues.SetValues(task);
    }
}

The SetParent function has to check if a task has a parent and make sure the parent isn't a new task (id == 0). It then tries to find the parent in the context version of the definition so that I don't end up with duplicates (ie - if the parent is not referenced it tries to add a new one even though it exists in the database). Once the parent is identified I check it's children to see if that task is already there, if not I add it.

private void SetParent(WorkflowContext context, WorkflowTask task)
    {
        if (task.ParentTask != null && task.ParentTask.WorkflowTaskId != 0)
        {
            var parentTask = context.WorkflowTasks.Where(t => t.WorkflowTaskId == task.ParentTask.WorkflowTaskId).FirstOrDefault();
            var parent = parentTask as ParentTask;
            task.ParentTask = parent;
            if (parentTask is ContainerTask)
            {
                var container = context.ContainerTasks.Where(c => c.WorkflowTaskId == parentTask.WorkflowTaskId).Include(c => c.ChildTasks).FirstOrDefault() as ContainerTask;
                if (container.ChildTasks == null)
                    container.ChildTasks = new List<WorkflowTask>();
                var childTask = container.ChildTasks.Find(t => t.WorkflowTaskId == task.WorkflowTaskId
                                                                && t.Order == task.Order);

                if(childTask == null)
                    container.ChildTasks.Add(task);
            }
        }
    }

One thing you will notice in the SetParent code is that I'm searching for a task by the ID and the Order. I had to do this because if I added two new children to a container both of the Ids would be zero and the second one wouldn't get added since it found the first one. Each task has a unique order so I used that to further differentiate them.

I don't feel super great about this code but I've been working on this problem for so long and this works so I'm going to leave it for now. I hope I covered all the information, I'm not too sure how many people will actually need this but you never know.

Zoey
  • 226
  • 2
  • 13