C# EF:包含所有子对象(包括子子对象)的复制对象(深度复制)

C# EF:包含所有子对象(包括子子对象)的复制对象(深度复制),c#,entity-framework,linq,linq-to-entities,C#,Entity Framework,Linq,Linq To Entities,我有一个具有如下属性的表: Id Name ParentId ParentId是主列Id的外键。现在让我们假设我有几行,如:(仅显示来自行的ParentId) 现在,假设我们要复制其ParentId为空的行对象及其所有子对象 var row = db.FirstOrDefault(x=> x.Id == 1); var new_row = new Table1(); var subrows = row.Table1.ToArray(); foreach(var

我有一个具有如下属性的表:

Id Name ParentId

ParentId
是主列
Id
的外键。现在让我们假设我有几行,如:(仅显示来自行的
ParentId

现在,假设我们要复制其
ParentId
为空的行对象及其所有子对象

  var row = db.FirstOrDefault(x=> x.Id == 1);
  var new_row = new Table1();
  var subrows = row.Table1.ToArray();
        foreach(var row in subrows)
        {
            db.Entry(row).State = System.Data.Entity.EntityState.Detached;
        }
        new_row.Table1 = subrows;
db.Table.Add(new_row);
db.saveChanges();
结果:新插入的结构如下:

    NULL
   /    \
  1      2
我假设只复制了一个子级别。如何复制/插入所有子级

编辑:由于“分离”有助于创建一个副本直到一个级别,因此我尝试了以下方法:

private void RecursiveDetach(Table1 parent)
        {
            var subrows = parent.Table1.ToArray();
            foreach (var row in subrows)
            {
                if(row.Table1.Count() > 0)
                {
                    RecursiveDetach(row);
                }
                db.Entry(row).State = System.Data.Entity.EntityState.Detached;
            }
        }
但是,现在我得到一个错误:

收集被修改;枚举操作不能执行


我以前不得不这么做。我完全是在代码中完成的,递归地复制对象并在需要的地方清理惟一的ID,但我所构建的最干净的方法是将对象序列化为XML,然后反序列化为新对象。这种方法效率较低,但非常灵活,易于实现

//Save object to XML file. Returns filename.
public string SaveObjectAsXML(int id)
{
    //however you get your EF context and disable proxy creation
    var db = GetContext(); 
    bool currentProxySetting = db.Configuration.ProxyCreationEnabled;
    db.Configuration.ProxyCreationEnabled = false;

    //get the data
    var item = db.GetItem(id); //retrieval be unique to your setup, but I have
                               //a more generic solution if you need it. Make
                               //sure you have all the sub items included
                               //in your object or they won't be saved.
    db.Configuration.ProxyCreationEnabled = currentProxySetting;

    //if no item is found, do whatever needs to be done
    if (item == null)
    {                
        return string.Empty;
    }            

    //I actually write my data to a file so I can save states if needed, but you could
    //modify the method to just spit out the XML instead
    Directory.CreateDirectory(DATA_PATH); //make sure path exists to prevent write errors
    string path = $"{DATA_PATH}{id}{DATA_EXT}";
    var bf = new BinaryFormatter();
    using (FileStream fs = new FileStream(path, FileMode.Create))
    {
        bf.Serialize(fs, repair);
    }

    return path;
}

//Load object from XML file. Returns ID.
public int LoadXMLData(string path)
{   
    //make sure the file exists
    if (!File.Exists(path))
    {
        throw new Exception("File not found.");
    }

    //load data from file
    try 
    { 
        using (FileStream fs = new FileStream(path, FileMode.Open)) 
        {
            var item = (YourItemType)new BinaryFormatter().Deserialize(fs);
            db.YourItemTypes.Add(item);
            db.SaveChanges();
            return item.Id;
        }
    }
    catch (Exception ex) {
        //Exceptions here are common when copying between databases where differences in config entries result in mis-matches
        throw;
    }
}
用法很简单

//save object
var savedObjectFilename = SaveObjectAsXML(myObjID);

//loading the item will create a copy
var newID = LoadXMLData(savedObjectFilename);

祝你好运

我以前不得不这么做。我完全是在代码中完成的,递归地复制对象并在需要的地方清理惟一的ID,但我所构建的最干净的方法是将对象序列化为XML,然后反序列化为新对象。这种方法效率较低,但非常灵活,易于实现

//Save object to XML file. Returns filename.
public string SaveObjectAsXML(int id)
{
    //however you get your EF context and disable proxy creation
    var db = GetContext(); 
    bool currentProxySetting = db.Configuration.ProxyCreationEnabled;
    db.Configuration.ProxyCreationEnabled = false;

    //get the data
    var item = db.GetItem(id); //retrieval be unique to your setup, but I have
                               //a more generic solution if you need it. Make
                               //sure you have all the sub items included
                               //in your object or they won't be saved.
    db.Configuration.ProxyCreationEnabled = currentProxySetting;

    //if no item is found, do whatever needs to be done
    if (item == null)
    {                
        return string.Empty;
    }            

    //I actually write my data to a file so I can save states if needed, but you could
    //modify the method to just spit out the XML instead
    Directory.CreateDirectory(DATA_PATH); //make sure path exists to prevent write errors
    string path = $"{DATA_PATH}{id}{DATA_EXT}";
    var bf = new BinaryFormatter();
    using (FileStream fs = new FileStream(path, FileMode.Create))
    {
        bf.Serialize(fs, repair);
    }

    return path;
}

//Load object from XML file. Returns ID.
public int LoadXMLData(string path)
{   
    //make sure the file exists
    if (!File.Exists(path))
    {
        throw new Exception("File not found.");
    }

    //load data from file
    try 
    { 
        using (FileStream fs = new FileStream(path, FileMode.Open)) 
        {
            var item = (YourItemType)new BinaryFormatter().Deserialize(fs);
            db.YourItemTypes.Add(item);
            db.SaveChanges();
            return item.Id;
        }
    }
    catch (Exception ex) {
        //Exceptions here are common when copying between databases where differences in config entries result in mis-matches
        throw;
    }
}
用法很简单

//save object
var savedObjectFilename = SaveObjectAsXML(myObjID);

//loading the item will create a copy
var newID = LoadXMLData(savedObjectFilename);

祝你好运

这里有第二个完全不同的答案:递归地分离整个对象,而不仅仅是父对象。以下内容是作为上下文对象的扩展方法编写的:

    /// <summary>
    /// Recursively detaches item and sub-items from EF. Assumes that all sub-objects are properties (not fields).
    /// </summary>
    /// <param name="item">The item to detach</param>
    /// <param name="recursionDepth">Number of levels to go before stopping. object.Property is 1, object.Property.SubProperty is 2, and so on.</param>
    public static void DetachAll(this DbContext db, object item, int recursionDepth = 3)
    {
        //Exit if no remaining recursion depth
        if (recursionDepth <= 0) return;

        //detach this object
        db.Entry(item).State = EntityState.Detached;

        //get reflection data for all the properties we mean to detach
        Type t = item.GetType();
        var properties = t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
                          .Where(p => p.GetSetMethod()?.IsPublic == true)  //get only properties we can set                              
                          .Where(p => p.PropertyType.IsClass)              //only classes can be EF objects
                          .Where(p => p.PropertyType != typeof(string))    //oh, strings. What a pain.
                          .Where(p => p.GetValue(item) != null);           //only get set properties

        //if we're recursing, we'll check here to make sure we should keep going
        if (properties.Count() == 0) return;

        foreach (var p in properties)
        {
            //handle generics
            if (p.PropertyType.IsGenericType)
            {
                //assume its Enumerable. More logic can be built here if that's not true.
                IEnumerable collection = (IEnumerable)p.GetValue(item);
                foreach (var obj in collection)
                {
                    db.Entry(obj).State = EntityState.Detached;
                    DetachAll(db, obj, recursionDepth - 1);
                }
            }
            else
            {
                var obj = p.GetValue(item);
                db.Entry(obj).State = EntityState.Detached;
                DetachAll(db, obj, recursionDepth - 1);
            }
        }
    }
要检索我的可查询集合,我现在将其称为:

var queryableOrders = context.GetQueryable(includes);
同样,这里的目的是创建一个可查询的对象,该对象将只急切地加载您实际需要的子对象(和子对象)

要获取特定项,请像使用任何其他可查询源一样使用:

var order = context.GetQueryable(includes).FirstOrDefault(o => o.OrderNumber == myOrderNumber);
请注意,您还可以提供内嵌的include表达式;但是,您需要指定泛型:

//you can provide includes inline if you just have a couple
var order = context.GetQueryable<Order>(o => o.Tenders, o => o.SalesItems).FirstOrDefault(o => o.OrderNumber == myOrderNumber);
//如果您只有一对,就可以提供includesinline
var order=context.GetQueryable(o=>o.Tenders,o=>o.SalesItems);

这里有第二个完全不同的答案:递归地分离整个对象,而不仅仅是父对象。以下内容是作为上下文对象的扩展方法编写的:

    /// <summary>
    /// Recursively detaches item and sub-items from EF. Assumes that all sub-objects are properties (not fields).
    /// </summary>
    /// <param name="item">The item to detach</param>
    /// <param name="recursionDepth">Number of levels to go before stopping. object.Property is 1, object.Property.SubProperty is 2, and so on.</param>
    public static void DetachAll(this DbContext db, object item, int recursionDepth = 3)
    {
        //Exit if no remaining recursion depth
        if (recursionDepth <= 0) return;

        //detach this object
        db.Entry(item).State = EntityState.Detached;

        //get reflection data for all the properties we mean to detach
        Type t = item.GetType();
        var properties = t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
                          .Where(p => p.GetSetMethod()?.IsPublic == true)  //get only properties we can set                              
                          .Where(p => p.PropertyType.IsClass)              //only classes can be EF objects
                          .Where(p => p.PropertyType != typeof(string))    //oh, strings. What a pain.
                          .Where(p => p.GetValue(item) != null);           //only get set properties

        //if we're recursing, we'll check here to make sure we should keep going
        if (properties.Count() == 0) return;

        foreach (var p in properties)
        {
            //handle generics
            if (p.PropertyType.IsGenericType)
            {
                //assume its Enumerable. More logic can be built here if that's not true.
                IEnumerable collection = (IEnumerable)p.GetValue(item);
                foreach (var obj in collection)
                {
                    db.Entry(obj).State = EntityState.Detached;
                    DetachAll(db, obj, recursionDepth - 1);
                }
            }
            else
            {
                var obj = p.GetValue(item);
                db.Entry(obj).State = EntityState.Detached;
                DetachAll(db, obj, recursionDepth - 1);
            }
        }
    }
要检索我的可查询集合,我现在将其称为:

var queryableOrders = context.GetQueryable(includes);
同样,这里的目的是创建一个可查询的对象,该对象将只急切地加载您实际需要的子对象(和子对象)

要获取特定项,请像使用任何其他可查询源一样使用:

var order = context.GetQueryable(includes).FirstOrDefault(o => o.OrderNumber == myOrderNumber);
请注意,您还可以提供内嵌的include表达式;但是,您需要指定泛型:

//you can provide includes inline if you just have a couple
var order = context.GetQueryable<Order>(o => o.Tenders, o => o.SalesItems).FirstOrDefault(o => o.OrderNumber == myOrderNumber);
//如果您只有一对,就可以提供includesinline
var order=context.GetQueryable(o=>o.Tenders,o=>o.SalesItems);

您是在处理自引用关系,还是涉及两个不同的实体?我不知道您想问什么,但这里有更多上下文
entities db=new entities()
这里是如何定义外键的
ALTER TABLE[dbo].[TABLE]WITH NOCHECK ADD CONSTRAINT[FK_TABLE_TABLE]外键([ParentId])引用[dbo].[TABLE]([Id])
您使用的是自引用关系还是涉及两个不同的实体?我不知道您想问什么,但这里有更多上下文
entities db=newentities()
这里是如何定义外键的
ALTER TABLE[dbo].[TABLE]WITH NOCHECK ADD CONSTRAINT[FK_TABLE\u TABLE]外键([ParentId])引用[dbo].[TABLE]([Id])
如果递归深度未知怎么办?我还用我尝试过的方法修改了这个问题。建议是否有一种简单的方法来解决集合修改错误。只要没有任何循环引用(即
Person.Address.State.Person
),就不需要递归检查。在这种情况下,您可以删除递归深度,也不会出现集合修改错误。这根本不起作用。它只返回父对象,而不返回子对象。一旦您分离父实体,子实体就不再可访问。@Shymalparikh,我不知道您的实现,但我怀疑它不起作用,因为您没有急切地加载所有数据。我已经更新了我的答案,详细说明了在使用此方法之前如何确保所有数据都已就绪。如果递归深度未知,该怎么办?我还用我尝试过的方法修改了这个问题。建议是否有一种简单的方法来解决集合修改错误。只要没有任何循环引用(即
Person.Address.State.Person
),就不需要递归检查。在这种情况下,您可以删除递归深度,也不会出现集合修改错误。这根本不起作用。它只返回父对象,而不返回子对象。一旦您分离父实体,子实体就不再可访问。@Shymalparikh,我不知道您的实现,但我怀疑它不起作用,因为您没有急切地加载所有数据。我已经更新了我的答案,详细说明了在使用此方法之前如何确保所有数据都已就绪。