这是第二个完全不同的答案:递归地分离整个对象,而不仅仅是父对象。以下是作为上下文对象的扩展方法编写的:
/// <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);
}
}
}
最需要注意的是配置类型属性——表示与对象不直接相关的数据的对象。这些可能会产生冲突,因此最好确保您的对象不包含它们。
注意:
这种方法需要预先填充您希望复制的所有子对象,避免延迟加载。为了确保这一点,我对我的 EF 查询使用了以下扩展:
//Given a custom context object such that CustomContext inherits from DbContext AND contains an arbitrary number of DbSet collections
//which represent the data in the database (i.e. DbSet<MyObject>), this method fetches a queryable collection of object type T which
//will preload sub-objects specified by the array of expressions (includeExpressions) in the form o => o.SubObject.
public static IQueryable<T> GetQueryable<T>(this CustomContext context, params Expression<Func<T, object>>[] includeExpressions) where T : class
{
//look through the context for a dbset of the specified type
var property = typeof(CustomContext).GetProperties().Where(p => p.PropertyType.IsGenericType &&
p.PropertyType.GetGenericArguments()[0] == typeof(T)).FirstOrDefault();
//if the property wasn't found, we don't have the queryable object. Throw exception
if (property == null) throw new Exception("No queryable context object found for Type " + typeof(T).Name);
//create a result of that type, then assign it to the dataset
IQueryable<T> source = (IQueryable<T>)property.GetValue(context);
//return
return includeExpressions.Aggregate(source, (current, expression) => current.Include(expression));
}
此方法假定您有一个自定义上下文对象,该对象继承自 DbContext 并包含您的对象的 DbSet<> 集合。它将找到合适的DbSet<T> 并返回一个可查询的集合,该集合将在您的对象中预加载指定的子类。这些被指定为表达式数组。例如:
//example for object type 'Order'
var includes = new Expression<Func<Order, object>>[] {
o => o.SalesItems.Select(p => p.Discounts), //load the 'SalesItems' collection AND the `Discounts` collection for each SalesItem
o => o.Config.PriceList, //load the Config object AND the PriceList sub-object
o => o.Tenders, //load the 'Tenders' collection
o => o.Customer //load the 'Customer' object
};
为了检索我的可查询集合,我现在这样称呼它:
var queryableOrders = context.GetQueryable(includes);
同样,这里的目的是创建一个可查询的对象,该对象将只急切地加载您真正想要的子对象(和子子对象)。
要获取特定项目,请像使用任何其他可查询来源一样使用它:
var order = context.GetQueryable(includes).FirstOrDefault(o => o.OrderNumber == myOrderNumber);
请注意,您还可以提供内联包含表达式;但是,您需要指定泛型:
//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);