views:

129

answers:

1

I am creating a Silverlight 4 application with Entity Framework, RIA Services and MVVM-Light toolkit. The application deals with a complex object graph, that contains the following structure:

  • Job 1->* Resources
  • Job 1->* Workplans
  • Workplan 1->* WorkplanItems
  • Resource 1->* Assignment
  • WorkplanItems 1->* Assignment

I would like to be able to load the job (using Include attributes/directive) and this works fine. to load the complete graph from the root of a specific job. However, when I submit changes back, I get an error

Entity for operation '0' has multiple parents

It has been my understanding from this post that the Composition attribute on my metadata is what should allow me to submit this as a complete graph and then handle the updating of all objects on the server properly with a single round trip from my silverlight application. My goal is to not submit changes with each change, but to allow the user to make a bunch of changes to the Job, and it's associated parts, then submit them, or cancel the changes.

Please let me know if you are aware of any issues that I have overlooked in this? Here is the metadata as I have it setup:

// The MetadataTypeAttribute identifies AssignmentMetadata as the class
// that carries additional metadata for the Assignment class.
[MetadataTypeAttribute(typeof(Assignment.AssignmentMetadata))]
public partial class Assignment
{

    // This class allows you to attach custom attributes to properties
    // of the Assignment class.
    //
    // For example, the following marks the Xyz property as a
    // required property and specifies the format for valid values:
    //    [Required]
    //    [RegularExpression("[A-Z][A-Za-z0-9]*")]
    //    [StringLength(32)]
    //    public string Xyz { get; set; }
    internal sealed class AssignmentMetadata
    {

        // Metadata classes are not meant to be instantiated.
        private AssignmentMetadata()
        {
        }

        public decimal CostBudgeted { get; set; }

        public decimal CostRemaining { get; set; }

        public decimal HoursBudgeted { get; set; }

        public decimal HoursRemaining { get; set; }

        public bool IsComplete { get; set; }

        public int ItemID { get; set; }

        public Job Job { get; set; }

        public int JobID { get; set; }

        public Resource Resource { get; set; }

        public int ResourceID { get; set; }

        public Workplan Workplan { get; set; }

        public int WorkplanID { get; set; }

        public WorkplanItem WorkplanItem { get; set; }
    }
}

// The MetadataTypeAttribute identifies JobMetadata as the class
// that carries additional metadata for the Job class.
[MetadataTypeAttribute(typeof(Job.JobMetadata))]
public partial class Job
{

    // This class allows you to attach custom attributes to properties
    // of the Job class.
    //
    // For example, the following marks the Xyz property as a
    // required property and specifies the format for valid values:
    //    [Required]
    //    [RegularExpression("[A-Z][A-Za-z0-9]*")]
    //    [StringLength(32)]
    //    public string Xyz { get; set; }
    internal sealed class JobMetadata
    {

        // Metadata classes are not meant to be instantiated.
        private JobMetadata()
        {
        }

        [Display(AutoGenerateField = false)]
        [Include]
        [Composition]
        public EntityCollection<Assignment> Assignments { get; set; }

        [Display(Name="Client Job", Order=2, Description="Is this a client job?")]
        [DefaultValue(true)]
        public bool IsRealJob { get; set; }

        [Display(AutoGenerateField = false)]
        [Include]
        public JobDetail JobDetail { get; set; }

        [Display(AutoGenerateField = false)]
        public int JoblID { get; set; }

        [Display(Name="Job Title", Order=1, Description="What should this job be called?")]
        public string Title { get; set; }

        [Display(AutoGenerateField = false)]
        [Include]
        [Composition]
        public EntityCollection<WorkplanItem> WorkplanItems { get; set; }

        [Display(AutoGenerateField = false)]
        [Include]
        [Composition]
        public EntityCollection<Workplan> Workplans { get; set; }


        [Display(AutoGenerateField = false)]
        [Include]
        [Composition]
        public EntityCollection<Resource> Resources { get; set; }
    }
}

// The MetadataTypeAttribute identifies JobDetailMetadata as the class
// that carries additional metadata for the JobDetail class.
[MetadataTypeAttribute(typeof(JobDetail.JobDetailMetadata))]
public partial class JobDetail
{

    // This class allows you to attach custom attributes to properties
    // of the JobDetail class.
    //
    // For example, the following marks the Xyz property as a
    // required property and specifies the format for valid values:
    //    [Required]
    //    [RegularExpression("[A-Z][A-Za-z0-9]*")]
    //    [StringLength(32)]
    //    public string Xyz { get; set; }
    internal sealed class JobDetailMetadata
    {

        // Metadata classes are not meant to be instantiated.
        private JobDetailMetadata()
        {
        }

        [Display(Name="Client", Order=1,Description="Name of the Client")]
        public string Client { get; set; }

        [Display(Name = "Client Fee", Order = 5, Description = "Client Fee from Engagement Letter")]
        [DisplayFormat(DataFormatString="C",NullDisplayText="<Not Set>",ApplyFormatInEditMode=true)]
        public Nullable<decimal> ClientFee { get; set; }

        [Display(AutoGenerateField=false)]
        public int ClientIndex { get; set; }

        [Display(AutoGenerateField = true)]
        public string EFOLDERID { get; set; }

        [Display(Name = "Engagement Name", Order = 4, Description = "Friendly name of the Engagement")]
        public string Engagement { get; set; }

        [Display(Name = "Eng Type", Order = 3, Description = "Type of Work being done")]
        public string EngagementType { get; set; }

        [Display(AutoGenerateField = false)]
        public Job Job { get; set; }

        [Display(AutoGenerateField = false)]
        public int JobID { get; set; }

        [Display(AutoGenerateField = false)]
        public int PEJobID { get; set; }

        [Display(Name = "Service", Order = 2, Description = "Service Type")]
        public string Service { get; set; }

        [Display(Name = "Timing of the Work", Order = 6, Description = "When will this work occur?")]
        public string Timing { get; set; }
    }
}

// The MetadataTypeAttribute identifies PendingTimesheetMetadata as the class
// that carries additional metadata for the PendingTimesheet class.
[MetadataTypeAttribute(typeof(PendingTimesheet.PendingTimesheetMetadata))]
public partial class PendingTimesheet
{

    // This class allows you to attach custom attributes to properties
    // of the PendingTimesheet class.
    //
    // For example, the following marks the Xyz property as a
    // required property and specifies the format for valid values:
    //    [Required]
    //    [RegularExpression("[A-Z][A-Za-z0-9]*")]
    //    [StringLength(32)]
    //    public string Xyz { get; set; }
    internal sealed class PendingTimesheetMetadata
    {

        // Metadata classes are not meant to be instantiated.
        private PendingTimesheetMetadata()
        {
        }

        public decimal PendingHours { get; set; }

        public string UserName { get; set; }

        public DateTime WorkDate { get; set; }

        [Include]
        public Workplan Workplan { get; set; }

        public int WorkplanID { get; set; }
    }
}

// The MetadataTypeAttribute identifies ResourceMetadata as the class
// that carries additional metadata for the Resource class.
[MetadataTypeAttribute(typeof(Resource.ResourceMetadata))]
public partial class Resource
{

    // This class allows you to attach custom attributes to properties
    // of the Resource class.
    //
    // For example, the following marks the Xyz property as a
    // required property and specifies the format for valid values:
    //    [Required]
    //    [RegularExpression("[A-Z][A-Za-z0-9]*")]
    //    [StringLength(32)]
    //    public string Xyz { get; set; }
    internal sealed class ResourceMetadata
    {

        // Metadata classes are not meant to be instantiated.
        private ResourceMetadata()
        {
        }

        [Include]
        [Composition]
        public EntityCollection<Assignment> Assignments { get; set; }

        [Include]
        public Job Job { get; set; }

        public int JobID { get; set; }

        public decimal Rate { get; set; }

        public int ResourceID { get; set; }

        public string Title { get; set; }

        public string UserName { get; set; }
    }
}

// The MetadataTypeAttribute identifies WorkplanMetadata as the class
// that carries additional metadata for the Workplan class.
[MetadataTypeAttribute(typeof(Workplan.WorkplanMetadata))]
public partial class Workplan
{

    // This class allows you to attach custom attributes to properties
    // of the Workplan class.
    //
    // For example, the following marks the Xyz property as a
    // required property and specifies the format for valid values:
    //    [Required]
    //    [RegularExpression("[A-Z][A-Za-z0-9]*")]
    //    [StringLength(32)]
    //    public string Xyz { get; set; }
    internal sealed class WorkplanMetadata
    {

        // Metadata classes are not meant to be instantiated.
        private WorkplanMetadata()
        {
        }

        [Include]
        [Composition]
        public EntityCollection<Assignment> Assignments { get; set; }

        public string Description { get; set; }

        [Include]
        public Job Job { get; set; }

        public int JobID { get; set; }

        public EntityCollection<PendingTimesheet> PendingTimesheets { get; set; }

        public Nullable<int> PETaskID { get; set; }

        public decimal TtlCost { get; set; }

        public decimal TtlHours { get; set; }

        public DateTime WorkEnd { get; set; }

        public int WorkplanID { get; set; }

        [Include]
        [Composition]
        public EntityCollection<WorkplanItem> WorkplanItems { get; set; }

        public DateTime WorkStart { get; set; }
    }
}

// The MetadataTypeAttribute identifies WorkplanItemMetadata as the class
// that carries additional metadata for the WorkplanItem class.
[MetadataTypeAttribute(typeof(WorkplanItem.WorkplanItemMetadata))]
public partial class WorkplanItem
{

    // This class allows you to attach custom attributes to properties
    // of the WorkplanItem class.
    //
    // For example, the following marks the Xyz property as a
    // required property and specifies the format for valid values:
    //    [Required]
    //    [RegularExpression("[A-Z][A-Za-z0-9]*")]
    //    [StringLength(32)]
    //    public string Xyz { get; set; }
    internal sealed class WorkplanItemMetadata
    {

        // Metadata classes are not meant to be instantiated.
        private WorkplanItemMetadata()
        {
        }

        [Include]
        [Composition]
        public EntityCollection<Assignment> Assignments { get; set; }

        public string Description { get; set; }

        public int ItemID { get; set; }

        [Include]
        public Job Job { get; set; }

        public int JobID { get; set; }

        public string Notes { get; set; }

        public short Ordinal { get; set; }

        [Include]
        public Workplan Workplan { get; set; }

        public int WorkplanID { get; set; }
    }
}
A: 

I have figured out the resolution to my problem. This is actually really powerful, once you understand some of these inner-working and caveats:

  1. Only apply the [Composition] and [Include] attribute to the root of your object graph.
  2. Your root object of the Composition is what gets submitted, and is where your update process starts.
  3. Your root object's EntityState may not actually represent an Update (as was my case).
  4. What ever first re-attaches your graph, all objects get attached and their state gets set to the same value (Inserted, Updated, Deleted, etc.)
  5. After your graph is re-attached you need to update each entity according to the ChangeSet that was submitted.

There may be more that I still don't completely understand; however, this does work in my environment. I would like to figure out a better way to do this, but I seem to run into different issues when I try to apply a common attach method.

Here is what I have come up with for my Update Method:

public void UpdateJob(Job currentJob)
    {
        //this.ObjectContext.Jobs.AttachAsModified(currentJob, this.ChangeSet.GetOriginal(currentJob));

        // compositional update process
        foreach (Assignment a in this.ChangeSet.GetAssociatedChanges(currentJob, j => j.Assignments))
        {
            ChangeOperation op = this.ChangeSet.GetChangeOperation(a);
            switch (op)
            {
                case ChangeOperation.Insert:
                    InsertAssignment(a);
                    break;
                case ChangeOperation.Update:
                    UpdateAssignment(a);
                    break;
                case ChangeOperation.Delete:
                    DeleteAssignment(a);
                    break;
                case ChangeOperation.None:
                    if (a.EntityState == EntityState.Detached)
                        this.ObjectContext.Assignments.Attach(a);
                    System.Data.Objects.ObjectStateEntry ose;
                    if (this.ObjectContext.ObjectStateManager.TryGetObjectStateEntry(a.EntityKey, out ose))
                        this.ObjectContext.ObjectStateManager.ChangeObjectState(a, EntityState.Unchanged);
                    break;
            }
        }

        foreach (WorkplanItem wpi in this.ChangeSet.GetAssociatedChanges(currentJob, j => j.WorkplanItems))
        {
            ChangeOperation op = this.ChangeSet.GetChangeOperation(wpi);
            switch (op)
            {
                case ChangeOperation.Insert:
                    InsertWorkplanItem(wpi);
                    break;
                case ChangeOperation.Update:
                    UpdateWorkplanItem(wpi);
                    break;
                case ChangeOperation.Delete:
                    DeleteWorkplanItem(wpi);
                    break;
                case ChangeOperation.None:
                    if (wpi.EntityState == EntityState.Detached)
                        this.ObjectContext.WorkplanItems.Attach(wpi);
                    System.Data.Objects.ObjectStateEntry ose;
                    if (this.ObjectContext.ObjectStateManager.TryGetObjectStateEntry(wpi.EntityKey, out ose))
                        this.ObjectContext.ObjectStateManager.ChangeObjectState(wpi, EntityState.Unchanged);
                    break;
            }
        }

        foreach (Workplan wp in this.ChangeSet.GetAssociatedChanges(currentJob, j => j.Workplans))
        {
            ChangeOperation op = this.ChangeSet.GetChangeOperation(wp);
            switch (op)
            {
                case ChangeOperation.Insert:
                    InsertWorkplan(wp);
                    break;
                case ChangeOperation.Update:
                    UpdateWorkplan(wp);
                    break;
                case ChangeOperation.Delete:
                    DeleteWorkplan(wp);
                    break;
                case ChangeOperation.None:
                    if (wp.EntityState == EntityState.Detached)
                        this.ObjectContext.Workplans.Attach(wp);
                    System.Data.Objects.ObjectStateEntry ose;
                    if (this.ObjectContext.ObjectStateManager.TryGetObjectStateEntry(wp.EntityKey, out ose))
                        this.ObjectContext.ObjectStateManager.ChangeObjectState(wp, EntityState.Unchanged);
                    break;
            }
        }

        foreach (Resource res in this.ChangeSet.GetAssociatedChanges(currentJob, j => j.Resources))
        {
            ChangeOperation op = this.ChangeSet.GetChangeOperation(res);
            switch (op)
            {
                case ChangeOperation.Insert:
                    InsertResource(res);
                    break;
                case ChangeOperation.Update:
                    UpdateResource(res);
                    break;
                case ChangeOperation.Delete:
                    DeleteResource(res);
                    break;
                case ChangeOperation.None:
                    if (res.EntityState == EntityState.Detached)
                        this.ObjectContext.Resources.Attach(res);
                    System.Data.Objects.ObjectStateEntry ose;
                    if (this.ObjectContext.ObjectStateManager.TryGetObjectStateEntry(res.EntityKey, out ose))
                        this.ObjectContext.ObjectStateManager.ChangeObjectState(res, EntityState.Unchanged);
                    break;
            }
        }

        ChangeOperation detailop = this.ChangeSet.GetChangeOperation(currentJob.JobDetail);
        switch (detailop)
        {
            case ChangeOperation.Insert:
                InsertJobDetail(currentJob.JobDetail);
                break;
            case ChangeOperation.Update:
                UpdateJobDetail(currentJob.JobDetail);
                break;
            case ChangeOperation.Delete:
                DeleteJobDetail(currentJob.JobDetail);
                break;
            case ChangeOperation.None:
                System.Data.Objects.ObjectStateEntry ose;
                if (this.ObjectContext.ObjectStateManager.TryGetObjectStateEntry(currentJob.JobDetail.EntityKey, out ose))
                        this.ObjectContext.ObjectStateManager.ChangeObjectState(currentJob.JobDetail, EntityState.Unchanged);
                break;
        }

        if (currentJob.EntityState == EntityState.Detached)
            this.ObjectContext.Jobs.Attach(currentJob);

        ChangeOperation jobop = this.ChangeSet.GetChangeOperation(currentJob);
        switch (jobop)
        {
            case ChangeOperation.Insert:
                InsertJob(currentJob);
                break;
            case ChangeOperation.Update:
                // Since this is the compositional root, we need to make sure there really is a change
                var origJob = this.ChangeSet.GetOriginal(currentJob);
                if (origJob != null)
                {
                    this.ObjectContext.Jobs.AttachAsModified(currentJob,
                        origJob);
                }
                else
                {
                    this.ObjectContext.ObjectStateManager.ChangeObjectState(
                        currentJob, EntityState.Unchanged);
                }
                break;
            case ChangeOperation.Delete:
                DeleteJob(currentJob);
                break;
            case ChangeOperation.None:
                this.ObjectContext.ObjectStateManager.ChangeObjectState(currentJob, EntityState.Unchanged);
                break;
        }

    }

In addition, here are the changes I had to make to the metadata classes for my entities.

// The MetadataTypeAttribute identifies AssignmentMetadata as the class
// that carries additional metadata for the Assignment class.
[MetadataTypeAttribute(typeof(Assignment.AssignmentMetadata))]
public partial class Assignment
{

    // This class allows you to attach custom attributes to properties
    // of the Assignment class.
    //
    // For example, the following marks the Xyz property as a
    // required property and specifies the format for valid values:
    //    [Required]
    //    [RegularExpression("[A-Z][A-Za-z0-9]*")]
    //    [StringLength(32)]
    //    public string Xyz { get; set; }
    internal sealed class AssignmentMetadata
    {

        // Metadata classes are not meant to be instantiated.
        private AssignmentMetadata()
        {
        }

        public decimal CostBudgeted { get; set; }

        public decimal CostRemaining { get; set; }

        public decimal HoursBudgeted { get; set; }

        public decimal HoursRemaining { get; set; }

        public bool IsComplete { get; set; }

        public int ItemID { get; set; }

        public Job Job { get; set; }

        public int JobID { get; set; }

        public Resource Resource { get; set; }

        public int ResourceID { get; set; }

        public Workplan Workplan { get; set; }

        public int WorkplanID { get; set; }

        public WorkplanItem WorkplanItem { get; set; }
    }
}

// The MetadataTypeAttribute identifies JobMetadata as the class
// that carries additional metadata for the Job class.
[MetadataTypeAttribute(typeof(Job.JobMetadata))]
public partial class Job
{

    // This class allows you to attach custom attributes to properties
    // of the Job class.
    //
    // For example, the following marks the Xyz property as a
    // required property and specifies the format for valid values:
    //    [Required]
    //    [RegularExpression("[A-Z][A-Za-z0-9]*")]
    //    [StringLength(32)]
    //    public string Xyz { get; set; }
    internal sealed class JobMetadata
    {

        // Metadata classes are not meant to be instantiated.
        private JobMetadata()
        {
        }

        [Display(AutoGenerateField = false)]
        [Include]
        [Composition]
        public EntityCollection<Assignment> Assignments { get; set; }

        [Display(Name="Client Job", Order=2, Description="Is this a client job?")]
        [DefaultValue(true)]
        public bool IsRealJob { get; set; }

        [Display(AutoGenerateField = false)]
        [Include]
        [Composition]
        public JobDetail JobDetail { get; set; }

        [Display(AutoGenerateField = false)]
        public int JoblID { get; set; }

        [Display(Name="Job Title", Order=1, Description="What should this job be called?")]
        public string Title { get; set; }

        [Display(AutoGenerateField = false)]
        [Include]
        [Composition]
        public EntityCollection<WorkplanItem> WorkplanItems { get; set; }

        [Display(AutoGenerateField = false)]
        [Include]
        [Composition]
        public EntityCollection<Workplan> Workplans { get; set; }


        [Display(AutoGenerateField = false)]
        [Include]
        [Composition]
        public EntityCollection<Resource> Resources { get; set; }
    }
}

// The MetadataTypeAttribute identifies JobDetailMetadata as the class
// that carries additional metadata for the JobDetail class.
[MetadataTypeAttribute(typeof(JobDetail.JobDetailMetadata))]
public partial class JobDetail
{

    // This class allows you to attach custom attributes to properties
    // of the JobDetail class.
    //
    // For example, the following marks the Xyz property as a
    // required property and specifies the format for valid values:
    //    [Required]
    //    [RegularExpression("[A-Z][A-Za-z0-9]*")]
    //    [StringLength(32)]
    //    public string Xyz { get; set; }
    internal sealed class JobDetailMetadata
    {

        // Metadata classes are not meant to be instantiated.
        private JobDetailMetadata()
        {
        }

        [Display(Name="Client", Order=1,Description="Name of the Client")]
        public string Client { get; set; }

        [Display(Name = "Client Fee", Order = 5, Description = "Client Fee from Engagement Letter")]
        [DisplayFormat(DataFormatString="C",NullDisplayText="<Not Set>",ApplyFormatInEditMode=true)]
        public Nullable<decimal> ClientFee { get; set; }

        [Display(AutoGenerateField=false)]
        public int ClientIndex { get; set; }

        [Display(AutoGenerateField = true)]
        public string EFOLDERID { get; set; }

        [Display(Name = "Engagement Name", Order = 4, Description = "Friendly name of the Engagement")]
        public string Engagement { get; set; }

        [Display(Name = "Eng Type", Order = 3, Description = "Type of Work being done")]
        public string EngagementType { get; set; }

        [Display(AutoGenerateField = false)]
        public Job Job { get; set; }

        [Display(AutoGenerateField = false)]
        public int JobID { get; set; }

        [Display(AutoGenerateField = false)]
        public int PEJobID { get; set; }

        [Display(Name = "Service", Order = 2, Description = "Service Type")]
        public string Service { get; set; }

        [Display(Name = "Timing of the Work", Order = 6, Description = "When will this work occur?")]
        public string Timing { get; set; }
    }
}

// The MetadataTypeAttribute identifies PendingTimesheetMetadata as the class
// that carries additional metadata for the PendingTimesheet class.
[MetadataTypeAttribute(typeof(PendingTimesheet.PendingTimesheetMetadata))]
public partial class PendingTimesheet
{

    // This class allows you to attach custom attributes to properties
    // of the PendingTimesheet class.
    //
    // For example, the following marks the Xyz property as a
    // required property and specifies the format for valid values:
    //    [Required]
    //    [RegularExpression("[A-Z][A-Za-z0-9]*")]
    //    [StringLength(32)]
    //    public string Xyz { get; set; }
    internal sealed class PendingTimesheetMetadata
    {

        // Metadata classes are not meant to be instantiated.
        private PendingTimesheetMetadata()
        {
        }

        public decimal PendingHours { get; set; }

        public string UserName { get; set; }

        public DateTime WorkDate { get; set; }

        [Include]
        public Workplan Workplan { get; set; }

        public int WorkplanID { get; set; }
    }
}

// The MetadataTypeAttribute identifies ResourceMetadata as the class
// that carries additional metadata for the Resource class.
[MetadataTypeAttribute(typeof(Resource.ResourceMetadata))]
public partial class Resource
{

    // This class allows you to attach custom attributes to properties
    // of the Resource class.
    //
    // For example, the following marks the Xyz property as a
    // required property and specifies the format for valid values:
    //    [Required]
    //    [RegularExpression("[A-Z][A-Za-z0-9]*")]
    //    [StringLength(32)]
    //    public string Xyz { get; set; }
    internal sealed class ResourceMetadata
    {

        // Metadata classes are not meant to be instantiated.
        private ResourceMetadata()
        {
        }

        public EntityCollection<Assignment> Assignments { get; set; }

        public Job Job { get; set; }

        public int JobID { get; set; }

        public decimal Rate { get; set; }

        public int ResourceID { get; set; }

        public string Title { get; set; }

        public string UserName { get; set; }
    }
}

// The MetadataTypeAttribute identifies WorkplanMetadata as the class
// that carries additional metadata for the Workplan class.
[MetadataTypeAttribute(typeof(Workplan.WorkplanMetadata))]
public partial class Workplan
{

    // This class allows you to attach custom attributes to properties
    // of the Workplan class.
    //
    // For example, the following marks the Xyz property as a
    // required property and specifies the format for valid values:
    //    [Required]
    //    [RegularExpression("[A-Z][A-Za-z0-9]*")]
    //    [StringLength(32)]
    //    public string Xyz { get; set; }
    internal sealed class WorkplanMetadata
    {

        // Metadata classes are not meant to be instantiated.
        private WorkplanMetadata()
        {
        }

        public EntityCollection<Assignment> Assignments { get; set; }

        public string Description { get; set; }

        public Job Job { get; set; }

        public int JobID { get; set; }

        public EntityCollection<PendingTimesheet> PendingTimesheets { get; set; }

        public Nullable<int> PETaskID { get; set; }

        public decimal TtlCost { get; set; }

        public decimal TtlHours { get; set; }

        public DateTime WorkEnd { get; set; }

        public int WorkplanID { get; set; }

        public EntityCollection<WorkplanItem> WorkplanItems { get; set; }

        public DateTime WorkStart { get; set; }
    }
}

// The MetadataTypeAttribute identifies WorkplanItemMetadata as the class
// that carries additional metadata for the WorkplanItem class.
[MetadataTypeAttribute(typeof(WorkplanItem.WorkplanItemMetadata))]
public partial class WorkplanItem
{

    // This class allows you to attach custom attributes to properties
    // of the WorkplanItem class.
    //
    // For example, the following marks the Xyz property as a
    // required property and specifies the format for valid values:
    //    [Required]
    //    [RegularExpression("[A-Z][A-Za-z0-9]*")]
    //    [StringLength(32)]
    //    public string Xyz { get; set; }
    internal sealed class WorkplanItemMetadata
    {

        // Metadata classes are not meant to be instantiated.
        private WorkplanItemMetadata()
        {
        }

        public EntityCollection<Assignment> Assignments { get; set; }

        public string Description { get; set; }

        public int ItemID { get; set; }

        public Job Job { get; set; }

        public int JobID { get; set; }

        public string Notes { get; set; }

        public short Ordinal { get; set; }

        public Workplan Workplan { get; set; }

        public int WorkplanID { get; set; }
    }
}

If anyone has other tips / ideas, please do add them. I will continue to post more as I learn more.

Ryan from Denver