5
votes

I am making an MVC4 web application using Entity Framework 5 (Database-first with generated POCOs) for data access.

In the app, the user goes through several screens, creating or editing a document (called a 'case study'). When they arrive at the final screen, their document exists as a CaseStudy POCO in memory, and everything is great until it is time to save this structure to the database.

To store the document, I have defined several database tables, which in turn map to EF POCOs used by the business layer, which is then consumed by the MVC controllers. As such, short-lived DbContexts are used to retrieve POCOs and store them in session between requests.

As a result, the save screen must save the contents of this POCO that has navigational properties to existing table data (Category, Layout, and Sections tables), and also added or updated data (CaseStudySections and the CaseStudy itself). So all of the POCOs are either new, or the context used to retrieve them has long been disposed. In other words, they are all 'detached'.

What is unusual about this post is that I already have a working solution in hand. The problem is that it is bulky, brittle, and inelegant. I am posting the code below. Note the iteration through sub-collections, the explicit adds and attaches, having to get an entry object and mark individual properties as modified just so they will be updated, and the awful song and dance at the end to get the AdditionalMaterials collection synced up. If this is what is required to deal with detached POCOs in EF5 I will be disappointed.

Am I missing something here? Is this consistent with best practices? Is there a more graceful and/or concise way to attach a structure of POCOs and insert/update?

The code to save a case study:

public void SaveCaseStudy(CaseStudy caseStudy)
{
    foreach (var s in caseStudy.CaseStudySections)
    {
        this.Entities.Sections.Attach(s.Section);

        if (s.CreatedByRefId == default(Guid))
        {
            s.CreatedByRefId = this.UserRefId;
            s.CreatedTime = DateTime.Now;
            this.Entities.CaseStudySections.Add(s);
        }
        else
        {
            this.Entities.CaseStudySections.Attach(s);
            var entry = this.Entities.Entry(s);
            entry.Property(e => e.TextData).IsModified = true;
            entry.Property(e => e.BinaryData).IsModified = true;
        }

        s.LastModifiedByRefId = this.UserRefId;
        s.LastModifiedTime = DateTime.Now;
    }

    foreach (var m in caseStudy.AdditionalMaterials)
    {
        if (m.CreatedByRefId == default(Guid))
        {
            m.CreatedByRefId = this.UserRefId;
            m.CreatedTime = DateTime.Now;
            this.Entities.AdditionalMaterials.Add(m);
        }
        else
        {
            this.Entities.AdditionalMaterials.Attach(m);
        }

        m.LastModifiedByRefId = this.UserRefId;
        m.LastModifiedByTime = DateTime.Now;
    }

    this.Entities.Layouts.Attach(caseStudy.Layout);
    this.Entities.Categories.Attach(caseStudy.Category);

    if (caseStudy.CreatedByRefId != default(Guid))
    {
        this.Entities.CaseStudies.Attach(caseStudy);
        var entry = this.Entities.Entry(caseStudy);
        entry.Property(e => e.CaseStudyName).IsModified = true;
        entry.Property(e => e.CaseStudyTitle).IsModified = true;
    }
    else
    {
        this.Entities.CaseStudies.Add(caseStudy);
        caseStudy.CreatedByRefId = this.UserRefId;
        caseStudy.CreatedTime = DateTime.Now;
    }

    caseStudy.LastModifiedByRefId = this.UserRefId;
    caseStudy.LastModifiedTime = DateTime.Now;

    if (caseStudy.CaseStudyStatus != (int)CaseStudyStatus.Personalized)
    {
        caseStudy.CaseStudyStatus = (int)CaseStudyStatus.PendingApproval;
    }

    caseStudy.ApprovedByRefId = null;
    caseStudy.ApprovedTime = null;
    this.Entities.SaveChanges();

    var existingAdditionalMaterialRefIds = caseStudy.AdditionalMaterials
        .Select(m => m.AdditionalMaterialRefId)
        .ToArray();

    var additionalMaterialsToRemove = this.Entities.AdditionalMaterials
        .Where(m => 
            m.CaseStudyRefId == caseStudy.CaseStudyRefId && 
            !existingAdditionalMaterialRefIds.Contains(m.AdditionalMaterialRefId))
        .ToArray();

    foreach (var additionalMaterialToRemove in additionalMaterialsToRemove)
    {
        this.Entities.AdditionalMaterials.Remove(additionalMaterialToRemove);
    }

    this.Entities.SaveChanges();
}
2

2 Answers

3
votes

In general it is what you have to do. You must tell EF about each change you want to perform when attaching detached object graph. I don't say that your code cannot be simplified but you will still have to deal with every entity and setting its state if you want it to be added or modified.

Here is little bit older but still valid answer about the topic - in short nothing has changes since I wrote it, only new DbContext API was created which still sits on top of the old API. The best description of this topic I have seen so far is in book Programming Entity Framework: DbContext.

2
votes

How about just doing:

db.CaseStudies.Attach(caseStudy);
db.Entry(caseStudy).State = EntityState.Modified;
db.SaveChange();

That will save all changes in your model to the db.