24
votes

Example structure

public class Page
{
    public int PageId { get; set; }
    public string Prop1 { get; set; }
    public string Prop2 { get; set; }
    public virtual List<Section> Sections { get; set; }
}

public class Section
{
    public int SectionId { get; set; }
    public int PageId { get; set; }
    public virtual Page Page { get; set; }
    public virtual List<Heading> Headings { get; set; }
}

public class Heading
{
    public int HeadingId { get; set; }
    public int SectionId { get; set; }
    public virtual Section Section { get; set; }
}

It's worth noting that my actual structure has more levels than this but this should be enough to explain what I'm trying to achieve.

So I load my Page object I then Clone that object and make some minor changes to the properties of Page i.e. Prop1, Prop2

Page pageFromDb = getPageMethod();
Page clonedPage = pageFromDb.Clone();
clonedPage.Prop1 = clonedPage.Prop1 + " Cloned";
addPageMethod(clonedPage); //Adds the page to db

In the example above clonedPage structure is fine and a new Page is added to the database. However I believe because the Id's of the child objects are set and the relationship of the children is always one to many. The original object pageFromDb will lose all it children as entity framework instead of creating new Section objects for the cloned Page will update the Section.PageId to the newly inserted page.

I believe a fix for this would be to foreach, foreach, etc. and set all the Id's to 0 before inserting then entity framework will create new records foreach object. Is there any easier/better way to clone an object in an entity framework environment.?

2
What does the Clone method do? Is it doing a deep copy? Can you tell it not to copy the Id Fields? As long as your collections are right, EF will build up the Ids for you after insertions.Tim
@Tim Just using AutoMapper at the top Page object level.Ashley Medway

2 Answers

45
votes

In order for Entity Framework to treat the clone as an entire new object graph when persisting the graph, all entities in the graph need to be disconnected from the context in which the root entity was retrieved.

This can be done using the AsNoTracking method on the context.

For example, this will pull a page and associated sections graph from the database and turn off tracking. Effectively this is a clone as if you add this to your Page DbSet and save it will create an entirely new object graph in the database. I.e. a new Page entity and new Section entities accordingly. Note, you wont need to call your Clone method.

var clone = context.Pages
    .AsNoTracking()
    .Including(pages => pages.Sections)
    .Single(...);
context.Pages.Add(clone);
context.SaveChanges(); // creates an entirely new object graph in the database
2
votes

Try this!

public Page CopyPage(int pageID)
{
    using(Context context = new Context())
    {
        context.Configuration.LazyLoadingEnabled = false;
        Page dbPage = context.Pages.Where(p => p.PageId == pageID).Include(s => s.Sections.Select(s => s.Section)).First();
        Page page = dbPage.Clone();
        page.PageId = 0;

        for (int i = 0; i < dbPage .Sections.Count; i++)
            page.Sections[i] = new Section
            {
                SectionId = 0,
                PageId = 0,
                Page = null,
                Headings = dbPage[i].Headings
            };
        return page;
    }
}

public Page Clone()
    {
        Object page = this.GetType().InvokeMember("", BindingFlags.CreateInstance, null, this, null);

        foreach(PropertyInfo propertyInfo in this.GetType().GetProperties())
        {
            if(propertyInfo.CanWrite)
            {
                propertyInfo.SetValue(page, propertyInfo.GetValue(this, null), null);
            }
        }

        return page;   
    }