Как обернуть Entity Framework для перехвата выражения LINQ перед выполнением?
Я хочу переписать некоторые части выражения LINQ непосредственно перед выполнением. И у меня проблемы с вложением моего переписывающего устройства в нужное место (вообще на самом деле).
Глядя на источник Entity Framework (в рефлекторе), он в конце сводится к IQueryProvider.Execute
, который в EF связан с выражением ObjectContext
, предлагающим свойство internal IQueryProvider Provider { get; }
.
Итак, я создал класс-оболочку (реализующий IQueryProvider
), чтобы выполнить переписывание Expression при вызове Execute, а затем передать его исходному провайдеру.
Проблема в том, что поле за Provider
равно private ObjectQueryProvider _queryProvider;
. Этот ObjectQueryProvider
является внутренним закрытым классом, что означает невозможность создания подкласса, предлагающего добавленную переписывание.
Таким образом, этот подход заставил меня зайти в тупик из-за очень тесно связанного объекта ObjectContext.
Как решить эту проблему? Я смотрю в неправильном направлении? Может ли быть способ внедрить себя вокруг этого ObjectQueryProvider
?
Обновить. Хотя предоставленные решения все работают, когда вы "обертываете" ObjectContext с использованием шаблона репозитория, было бы предпочтительным решение, которое позволяло бы прямое использование сгенерированного подкласса из ObjectContext. Таким образом, они остаются совместимыми с лесами Dynamic Data.
Ответы
Ответ 1
Основываясь на ответе Артура, я создал рабочую оболочку.
Предоставляемые фрагменты предоставляют способ обернуть каждый запрос LINQ с помощью собственного QueryProvider и корня IQueryable. Это означало бы, что у вас должен быть контроль над начальным запуском запроса (поскольку вы будете использовать большую часть времени, используя какой-либо шаблон).
Проблема с этим методом заключается в том, что он не прозрачен, более идеальной ситуацией было бы впрыскивать что-то в контейнер сущностей на уровне конструктора.
Я создал компилируемую реализацию, заставил ее работать с инфраструктурой сущностей и добавил поддержку метода ObjectQuery.Include. Класс посетителей выражения можно скопировать из MSDN.
public class QueryTranslator<T> : IOrderedQueryable<T>
{
private Expression expression = null;
private QueryTranslatorProvider<T> provider = null;
public QueryTranslator(IQueryable source)
{
expression = Expression.Constant(this);
provider = new QueryTranslatorProvider<T>(source);
}
public QueryTranslator(IQueryable source, Expression e)
{
if (e == null) throw new ArgumentNullException("e");
expression = e;
provider = new QueryTranslatorProvider<T>(source);
}
public IEnumerator<T> GetEnumerator()
{
return ((IEnumerable<T>)provider.ExecuteEnumerable(this.expression)).GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return provider.ExecuteEnumerable(this.expression).GetEnumerator();
}
public QueryTranslator<T> Include(String path)
{
ObjectQuery<T> possibleObjectQuery = provider.source as ObjectQuery<T>;
if (possibleObjectQuery != null)
{
return new QueryTranslator<T>(possibleObjectQuery.Include(path));
}
else
{
throw new InvalidOperationException("The Include should only happen at the beginning of a LINQ expression");
}
}
public Type ElementType
{
get { return typeof(T); }
}
public Expression Expression
{
get { return expression; }
}
public IQueryProvider Provider
{
get { return provider; }
}
}
public class QueryTranslatorProvider<T> : ExpressionVisitor, IQueryProvider
{
internal IQueryable source;
public QueryTranslatorProvider(IQueryable source)
{
if (source == null) throw new ArgumentNullException("source");
this.source = source;
}
public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
{
if (expression == null) throw new ArgumentNullException("expression");
return new QueryTranslator<TElement>(source, expression) as IQueryable<TElement>;
}
public IQueryable CreateQuery(Expression expression)
{
if (expression == null) throw new ArgumentNullException("expression");
Type elementType = expression.Type.GetGenericArguments().First();
IQueryable result = (IQueryable)Activator.CreateInstance(typeof(QueryTranslator<>).MakeGenericType(elementType),
new object[] { source, expression });
return result;
}
public TResult Execute<TResult>(Expression expression)
{
if (expression == null) throw new ArgumentNullException("expression");
object result = (this as IQueryProvider).Execute(expression);
return (TResult)result;
}
public object Execute(Expression expression)
{
if (expression == null) throw new ArgumentNullException("expression");
Expression translated = this.Visit(expression);
return source.Provider.Execute(translated);
}
internal IEnumerable ExecuteEnumerable(Expression expression)
{
if (expression == null) throw new ArgumentNullException("expression");
Expression translated = this.Visit(expression);
return source.Provider.CreateQuery(translated);
}
#region Visitors
protected override Expression VisitConstant(ConstantExpression c)
{
// fix up the Expression tree to work with EF again
if (c.Type == typeof(QueryTranslator<T>))
{
return source.Expression;
}
else
{
return base.VisitConstant(c);
}
}
#endregion
}
Пример использования в вашем репозитории:
public IQueryable<User> List()
{
return new QueryTranslator<User>(entities.Users).Include("Department");
}
Ответ 2
У меня есть именно тот исходный код, который вам понадобится - но не знаю, как подключить файл.
Вот некоторые фрагменты (фрагменты! Мне пришлось адаптировать этот код, чтобы он не компилировался):
IQueryable:
public class QueryTranslator<T> : IOrderedQueryable<T>
{
private Expression _expression = null;
private QueryTranslatorProvider<T> _provider = null;
public QueryTranslator(IQueryable source)
{
_expression = Expression.Constant(this);
_provider = new QueryTranslatorProvider<T>(source);
}
public QueryTranslator(IQueryable source, Expression e)
{
if (e == null) throw new ArgumentNullException("e");
_expression = e;
_provider = new QueryTranslatorProvider<T>(source);
}
public IEnumerator<T> GetEnumerator()
{
return ((IEnumerable<T>)_provider.ExecuteEnumerable(this._expression)).GetEnumerator();
}
IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return _provider.ExecuteEnumerable(this._expression).GetEnumerator();
}
public Type ElementType
{
get { return typeof(T); }
}
public Expression Expression
{
get { return _expression; }
}
public IQueryProvider Provider
{
get { return _provider; }
}
}
IQueryProvider:
public class QueryTranslatorProvider<T> : ExpressionTreeTranslator, IQueryProvider
{
IQueryable _source;
public QueryTranslatorProvider(IQueryable source)
{
if (source == null) throw new ArgumentNullException("source");
_source = source;
}
#region IQueryProvider Members
public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
{
if (expression == null) throw new ArgumentNullException("expression");
return new QueryTranslator<TElement>(_source, expression) as IQueryable<TElement>;
}
public IQueryable CreateQuery(Expression expression)
{
if (expression == null) throw new ArgumentNullException("expression");
Type elementType = expression.Type.FindElementTypes().First();
IQueryable result = (IQueryable)Activator.CreateInstance(typeof(QueryTranslator<>).MakeGenericType(elementType),
new object[] { _source, expression });
return result;
}
public TResult Execute<TResult>(Expression expression)
{
if (expression == null) throw new ArgumentNullException("expression");
object result = (this as IQueryProvider).Execute(expression);
return (TResult)result;
}
public object Execute(Expression expression)
{
if (expression == null) throw new ArgumentNullException("expression");
Expression translated = this.Visit(expression);
return _source.Provider.Execute(translated);
}
internal IEnumerable ExecuteEnumerable(Expression expression)
{
if (expression == null) throw new ArgumentNullException("expression");
Expression translated = this.Visit(expression);
return _source.Provider.CreateQuery(translated);
}
#endregion
#region Visits
protected override MethodCallExpression VisitMethodCall(MethodCallExpression m)
{
return m;
}
protected override Expression VisitUnary(UnaryExpression u)
{
return Expression.MakeUnary(u.NodeType, base.Visit(u.Operand), u.Type.ToImplementationType(), u.Method);
}
#endregion
}
Использование (предупреждение: адаптированный код! Может не компилироваться):
private Dictionary<Type, object> _table = new Dictionary<Type, object>();
public override IQueryable<T> GetObjectQuery<T>()
{
if (!_table.ContainsKey(type))
{
_table[type] = new QueryTranslator<T>(
_ctx.CreateQuery<T>("[" + typeof(T).Name + "]"));
}
return (IQueryable<T>)_table[type];
}
Выражение посетителей/переводчика:
http://blogs.msdn.com/mattwar/archive/2007/07/31/linq-building-an-iqueryable-provider-part-ii.aspx
http://msdn.microsoft.com/en-us/library/bb882521.aspx
EDIT: добавлен FindElementTypes(). Надеюсь, теперь все методы присутствуют.
/// <summary>
/// Finds all implemented IEnumerables of the given Type
/// </summary>
public static IQueryable<Type> FindIEnumerables(this Type seqType)
{
if (seqType == null || seqType == typeof(object) || seqType == typeof(string))
return new Type[] { }.AsQueryable();
if (seqType.IsArray || seqType == typeof(IEnumerable))
return new Type[] { typeof(IEnumerable) }.AsQueryable();
if (seqType.IsGenericType && seqType.GetGenericArguments().Length == 1 && seqType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
return new Type[] { seqType, typeof(IEnumerable) }.AsQueryable();
}
var result = new List<Type>();
foreach (var iface in (seqType.GetInterfaces() ?? new Type[] { }))
{
result.AddRange(FindIEnumerables(iface));
}
return FindIEnumerables(seqType.BaseType).Union(result);
}
/// <summary>
/// Finds all element types provided by a specified sequence type.
/// "Element types" are T for IEnumerable<T> and object for IEnumerable.
/// </summary>
public static IQueryable<Type> FindElementTypes(this Type seqType)
{
return seqType.FindIEnumerables().Select(t => t.IsGenericType ? t.GetGenericArguments().Single() : typeof(object));
}
Ответ 3
Просто хотел добавить к примеру Артура.
Как предупреждал Артур, в его методе GetObjectQuery() есть ошибка.
Он создает базовый запрос с использованием typeof (T).Name как имя EntitySet.
Имя EntitySet отличается от имени типа.
Если вы используете EF 4, вы должны сделать это:
public override IQueryable<T> GetObjectQuery<T>()
{
if (!_table.ContainsKey(type))
{
_table[type] = new QueryTranslator<T>(
_ctx.CreateObjectSet<T>();
}
return (IQueryable<T>)_table[type];
}
Что работает, если у вас нет Множественных наборов объектов для каждого типа (MEST), что очень редко.
Если вы используете 3.5, вы можете использовать код в Tip 13, чтобы получить имя EntitySet и подать его следующим образом:
public override IQueryable<T> GetObjectQuery<T>()
{
if (!_table.ContainsKey(type))
{
_table[type] = new QueryTranslator<T>(
_ctx.CreateQuery<T>("[" + GetEntitySetName<T>() + "]"));
}
return (IQueryable<T>)_table[type];
}
Надеюсь, что это поможет
Алекс
Советы по платформе Entity Framework