Ответ 1
После долгих исследований я наконец нашел способ добиться того, чего хотел. Суть его в том, что я перехватываю материализованные объекты с обработчиком событий в контексте объекта, а затем внедряю свой собственный класс коллекции в каждое свойство коллекции, которое я могу найти (с отражением).
Наиболее важной частью является перехват "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
}
Если вы пропустите его, вы обнаружите, что наиболее важной частью является свойство "Entities", которое будет ленить загружать фактические значения. В конструкторе FilteredCollection я передаю необязательный ICollection для сценария, в котором коллекция уже загружена.
Конечно, нам еще нужно настроить Entity Framework, чтобы наш FilteredCollection использовался везде, где есть свойства коллекции. Это может быть достигнуто путем подключения к событию ObjectMaterialized базового объекта ObjectContext для платформы Entity Framework:
(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. Это приемлемый недостаток, хотя мне еще предстоит провести некоторое тестирование того, как это влияет на метод 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;//.GetGenericArguments().First();
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
Однако все же могут быть некоторые ошибки, так как это очень большая работа. Концептуальная идея звучит, хотя, и я ожидаю, что она будет полностью функционировать вскоре после того, как я аккуратно переработаю все и найду время, чтобы написать несколько тестов для этого.