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 I
ve 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 :-)