经过大量研究,我终于找到了实现想要的功能的方法。简而言之,我通过在对象上下文中使用事件处理程序截取物化实体,并注入自定义集合类到可以找到的每个集合属性中(借助反射)。
最重要的部分是拦截“DbCollectionEntry”,该类负责加载相关集合属性。通过在实体和DbCollectionEntry之间插入自己,我完全控制了何时以及如何加载内容。唯一的缺点是这个DbCollectionEntry类几乎没有公开成员,这需要我使用反射来操作它。
以下是我自定义的实现ICollection接口并包含对应的DbCollectionEntry引用的集合类:
public class FilteredCollection <TEntity> : ICollection<TEntity> where TEntity : Entity
{
private readonly DbCollectionEntry _dbCollectionEntry;
private readonly Func<TEntity, Boolean> _compiledFilter;
private readonly Expression<Func<TEntity, Boolean>> _filter;
private ICollection<TEntity> _collection;
private int? _cachedCount;
public FilteredCollection(ICollection<TEntity> collection, DbCollectionEntry dbCollectionEntry)
{
_filter = entity => !entity.Deleted;
_dbCollectionEntry = dbCollectionEntry;
_compiledFilter = _filter.Compile();
_collection = collection != null ? collection.Where(_compiledFilter).ToList() : null;
}
private ICollection<TEntity> Entities
{
get
{
if (_dbCollectionEntry.IsLoaded == false && _collection == null)
{
IQueryable<TEntity> query = _dbCollectionEntry.Query().Cast<TEntity>().Where(_filter);
_dbCollectionEntry.CurrentValue = this;
_collection = query.ToList();
object internalCollectionEntry =
_dbCollectionEntry.GetType()
.GetField("_internalCollectionEntry", BindingFlags.NonPublic | BindingFlags.Instance)
.GetValue(_dbCollectionEntry);
object relatedEnd =
internalCollectionEntry.GetType()
.BaseType.GetField("_relatedEnd", BindingFlags.NonPublic | BindingFlags.Instance)
.GetValue(internalCollectionEntry);
relatedEnd.GetType()
.GetField("_isLoaded", BindingFlags.NonPublic | BindingFlags.Instance)
.SetValue(relatedEnd, true);
}
return _collection;
}
}
#region ICollection<T> Members
void ICollection<TEntity>.Add(TEntity item)
{
if(_compiledFilter(item))
Entities.Add(item);
}
void ICollection<TEntity>.Clear()
{
Entities.Clear();
}
Boolean ICollection<TEntity>.Contains(TEntity item)
{
return Entities.Contains(item);
}
void ICollection<TEntity>.CopyTo(TEntity[] array, Int32 arrayIndex)
{
Entities.CopyTo(array, arrayIndex);
}
Int32 ICollection<TEntity>.Count
{
get
{
if (_dbCollectionEntry.IsLoaded)
return _collection.Count;
return _dbCollectionEntry.Query().Cast<TEntity>().Count(_filter);
}
}
Boolean ICollection<TEntity>.IsReadOnly
{
get
{
return Entities.IsReadOnly;
}
}
Boolean ICollection<TEntity>.Remove(TEntity item)
{
return Entities.Remove(item);
}
#endregion
#region IEnumerable<T> Members
IEnumerator<TEntity> IEnumerable<TEntity>.GetEnumerator()
{
return Entities.GetEnumerator();
}
#endregion
#region IEnumerable Members
IEnumerator IEnumerable.GetEnumerator()
{
return ( ( this as IEnumerable<TEntity> ).GetEnumerator() );
}
#endregion
}
如果您仔细浏览,会发现最重要的部分是“实体”属性,它将延迟加载实际值。在FilterCollection的构造函数中,我传递了一个可选的ICollection,用于已经急切地加载集合的场景。
当然,我们仍然需要配置Entity Framework,以便我们的FilteredCollection在存在集合属性的任何地方都可以使用。这可以通过钩入Entity Framework的底层ObjectContext的ObjectMaterialized事件来实现。
(this as IObjectContextAdapter).ObjectContext.ObjectMaterialized +=
delegate(Object sender, ObjectMaterializedEventArgs e)
{
if (e.Entity is Entity)
{
var entityType = e.Entity.GetType();
IEnumerable<PropertyInfo> collectionProperties;
if (!CollectionPropertiesPerType.TryGetValue(entityType, out collectionProperties))
{
CollectionPropertiesPerType[entityType] = (collectionProperties = entityType.GetProperties()
.Where(p => p.PropertyType.IsGenericType && typeof(ICollection<>) == p.PropertyType.GetGenericTypeDefinition()));
}
foreach (var collectionProperty in collectionProperties)
{
var collectionType = typeof(FilteredCollection<>).MakeGenericType(collectionProperty.PropertyType.GetGenericArguments());
DbCollectionEntry dbCollectionEntry = Entry(e.Entity).Collection(collectionProperty.Name);
dbCollectionEntry.CurrentValue = Activator.CreateInstance(collectionType, new[] { dbCollectionEntry.CurrentValue, dbCollectionEntry });
}
}
};
看起来相当复杂,但它的本质是扫描物化类型以获取集合属性并将值更改为过滤集合。它还将DbCollectionEntry传递给过滤集合,以便其发挥作用。
这涵盖了整个“加载实体”的部分。到目前为止唯一的缺点是急切加载的集合属性仍将包括已删除的实体,但它们在FilterCollection类的“Add”方法中被过滤掉。尽管我还没有对这如何影响SaveChanges()方法进行测试,但这是可以接受的缺点。
当然,这仍然存在一个问题:查询时没有自动过滤。如果您想获取过去一周进行锻炼的健身房会员,您需要自动排除已删除的锻炼。
这是通过ExpressionVisitor实现的,它会自动将“.Where(e =>!e.Deleted)”过滤器应用于给定表达式中找到的每个IQueryable。
以下是代码:
public class DeletedFilterInterceptor: ExpressionVisitor
{
public Expression<Func<Entity, bool>> Filter { get; set; }
public DeletedFilterInterceptor()
{
Filter = entity => !entity.Deleted;
}
protected override Expression VisitMember(MemberExpression ex)
{
return !ex.Type.IsGenericType ? base.VisitMember(ex) : CreateWhereExpression(Filter, ex) ?? base.VisitMember(ex);
}
private Expression CreateWhereExpression(Expression<Func<Entity, bool>> filter, Expression ex)
{
var type = ex.Type;
var test = CreateExpression(filter, type);
if (test == null)
return null;
var listType = typeof(IQueryable<>).MakeGenericType(type);
return Expression.Convert(Expression.Call(typeof(Enumerable), "Where", new Type[] { type }, (Expression)ex, test), listType);
}
private LambdaExpression CreateExpression(Expression<Func<Entity, bool>> condition, Type type)
{
var lambda = (LambdaExpression) condition;
if (!typeof(Entity).IsAssignableFrom(type))
return null;
var newParams = new[] { Expression.Parameter(type, "entity") };
var paramMap = lambda.Parameters.Select((original, i) => new { original, replacement = newParams[i] }).ToDictionary(p => p.original, p => p.replacement);
var fixedBody = ParameterRebinder.ReplaceParameters(paramMap, lambda.Body);
lambda = Expression.Lambda(fixedBody, newParams);
return lambda;
}
}
public class ParameterRebinder : ExpressionVisitor
{
private readonly Dictionary<ParameterExpression, ParameterExpression> _map;
public ParameterRebinder(Dictionary<ParameterExpression, ParameterExpression> map)
{
_map = map ?? new Dictionary<ParameterExpression, ParameterExpression>();
}
public static Expression ReplaceParameters(Dictionary<ParameterExpression, ParameterExpression> map, Expression exp)
{
return new ParameterRebinder(map).Visit(exp);
}
protected override Expression VisitParameter(ParameterExpression node)
{
ParameterExpression replacement;
if (_map.TryGetValue(node, out replacement))
node = replacement;
return base.VisitParameter(node);
}
}
时间有点紧,稍后我会用更多的细节回到这篇文章,但它的主旨已经写好了。如果你们中有人急于尝试一切,我已经在这里发布了完整的测试应用程序:https://github.com/amoerie/TestingGround
然而,由于这还是一个正在进行中的工作,可能仍然存在一些错误。不过,概念上的想法是可行的,一旦我整理好所有东西并有时间为此编写一些测试,我希望它很快就能完全运行。