Entity Framework 6 Update Graph

c# entity-framework entity-framework-6 sql-server-ce-4

Question

What is the proper technique to store a graph of unknown-state objects? State refers to whether the database entries are brand-new or already present and being updated.

If I have, for instance:

public class Person
{
     public int Id { get; set; }
     public int Name { get; set; }
     public virtual ICollection<Automobile> Automobiles { get; set; }

}

public class Automobile
{
     public int Id { get; set; }
     public int Name { get; set; }
     public short Seats { get; set; }
     public virtual ICollection<MaintenanceRecord> MaintenanceRecords { get; set ;}
     public virtual Person Person { get; set; }
}

public class MaintenanceRecord
{
     public int Id { get; set; }
     public int AutomobileId { get; set; }
     public DateTime DatePerformed { get; set; }

     public virtual Automobile Automobile{ get; set; }

}

Similar to these aforementioned objects, I'm editing models, and after updating them, I transfer those models to the data layer, where I'm utilizing entity framework to store the models. So I'm converting these models into DAL-specific POCO entities.

It seems like I have a lot of work to do to "Save" the changes unless my models include a state indicating whether they are new or updated. I must first choose the Person entity and edit it before matching any existing Automobiles and updating those while adding any new ones. I must then check for any new or updated maintenance records for each Automobile.

Is there a quicker or simpler way to complete this? I suppose keeping track of the Model state would be beneficial in this situation, but doing so would require me to make modifications to the code outside of the data layer, which is something I'd rather avoid. I'm simply hoping there's a pattern of usage for updates like this somewhere that I can look up.

1
12
6/2/2014 5:07:17 PM

Accepted Answer

I've been following this thread on the EF Codeplex website since I first encountered this problem a while back. https://entityframework.codeplex.com/workitem/864

It appears to be under consideration for the upcoming release—I'm guessing EF 7—which is reportedly a significant internal rework of EF. Checking this out might be worthwhile. http://www.nuget.org/packages/RefactorThis.GraphDiff/

When I started working on this, I saw another EF post on SO where someone had provided a manual implementation example. I don't remember why I chose to do it manually at the time; GraphDiff appears to be a really cool tool. Here's an illustration of what I did.

  public async Task<IHttpActionResult> PutAsync([FromBody] WellEntityModel model)
    {
        try
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
            var kne = TheContext.Companies.First();
            var entity = TheModelFactory.Create(model);
            entity.DateUpdated = DateTime.Now;

            var currentWell = TheContext.Wells.Find(model.Id);

            // Update scalar/complex properties of parent
            TheContext.Entry(currentWell).CurrentValues.SetValues(entity);

            //We don't pass back the company so need to attached the associated company... this is done after mapping the values to ensure its not null.
            currentWell.Company = kne;

            // Updated geometry - ARGHHH NOOOOOO check on this once in a while for a fix from EF-Team https://entityframework.codeplex.com/workitem/864
            var geometryItemsInDb = currentWell.Geometries.ToList();
            foreach (var geometryInDb in geometryItemsInDb)
            {
                // Is the geometry item still there?
                var geometry = entity.Geometries.SingleOrDefault(i => i.Id == geometryInDb.Id);
                if (geometry != null)
                    // Yes: Update scalar/complex properties of child
                    TheContext.Entry(geometryInDb).CurrentValues.SetValues(geometry);
                else
                    // No: Delete it
                    TheContext.WellGeometryItems.Remove(geometryInDb);
            }
            foreach (var geometry in entity.Geometries)
            {
                // Is the child NOT in DB?
                if (geometryItemsInDb.All(i => i.Id != geometry.Id))
                    // Yes: Add it as a new child
                    currentWell.Geometries.Add(geometry);
            }

            // Update Surveys
            var surveyPointsInDb = currentWell.SurveyPoints.ToList();
            foreach (var surveyInDb in surveyPointsInDb)
            {
                // Is the geometry item still there?
                var survey = entity.SurveyPoints.SingleOrDefault(i => i.Id == surveyInDb.Id);
                if (survey != null)
                    // Yes: Update scalar/complex properties of child
                    TheContext.Entry(surveyInDb).CurrentValues.SetValues(survey);
                else
                    // No: Delete it
                    TheContext.WellSurveyPoints.Remove(surveyInDb);
            }
            foreach (var survey in entity.SurveyPoints)
            {
                // Is the child NOT in DB?
                if (surveyPointsInDb.All(i => i.Id != survey.Id))
                    // Yes: Add it as a new child
                    currentWell.SurveyPoints.Add(survey);
            }

            // Update Temperatures - THIS IS A HUGE PAIN = HOPE EF is updated to handle updating disconnected graphs.
            var temperaturesInDb = currentWell.Temperatures.ToList();
            foreach (var tempInDb in temperaturesInDb)
            {
                // Is the geometry item still there?
                var temperature = entity.Temperatures.SingleOrDefault(i => i.Id == tempInDb.Id);
                if (temperature != null)
                    // Yes: Update scalar/complex properties of child
                    TheContext.Entry(tempInDb).CurrentValues.SetValues(temperature);
                else
                    // No: Delete it
                    TheContext.WellTemperaturePoints.Remove(tempInDb);
            }
            foreach (var temps in entity.Temperatures)
            {
                // Is the child NOT in DB?
                if (surveyPointsInDb.All(i => i.Id != temps.Id))
                    // Yes: Add it as a new child
                    currentWell.Temperatures.Add(temps);
            }
            await TheContext.SaveChangesAsync();
            return Ok(model);
        }
        catch (Exception ex)
        {
            Trace.WriteLine(ex.Message);
        }
        return InternalServerError();
    }
7
6/2/2014 5:29:56 PM

Popular Answer

I also find this to be a major pain. I converted the response from @GetFuzzy to a more practical technique:

public void UpdateCollection<TCollection, TKey>(
    DbContext context, IList<TCollection> databaseCollection, 
    IList<TCollection> detachedCollection, 
    Func<TCollection, TKey> keySelector) where TCollection: class where TKey: IEquatable<TKey>
{
    var databaseCollectionClone = databaseCollection.ToArray();
    foreach (var databaseItem in databaseCollectionClone)
    {
        var detachedItem = detachedCollection.SingleOrDefault(item => keySelector(item).Equals(keySelector(databaseItem)));
        if (detachedItem != null)
        {
            context.Entry(databaseItem).CurrentValues.SetValues(detachedItem);
        }
        else
        {
            context.Set<TCollection>().Remove(databaseItem);
        }
    }

    foreach (var detachedItem in detachedCollection)
    {
        if (databaseCollectionClone.All(item => keySelector(item).Equals(keySelector(detachedItem)) == false))
        {
            databaseCollection.Add(detachedItem);
        }
    }
}

With this technique in place, I can employ it as follows:

public void UpdateProduct(Product product)
{
   ...

   var databaseProduct = productRepository.GetById(product.Id);

   UpdateCollection(context, databaseProduct.Accessories, product.Accessories, productAccessory => productAcccessory.ProductAccessoryId);
   UpdateCollection(context, databaseProduct.Categories, product.Categories, productCategory => productCategory.ProductCategoryId);

   ...

   context.SubmitChanges();
}

However, I have a suspicion that this won't be enough as the graph becomes deeper.



Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow