Как обернуть Entity Framework для перехвата выражения LINQ непосредственно перед выполнением?

Я хочу переписать определенные части выражения LINQ непосредственно перед выполнением. И у меня проблемы с вводом моего рерайтера в нужное место (на самом деле вообще).

Глядя на источник Entity Framework (в рефлекторе), он в конце концов сводится к IQueryProvider.Execute, который в EF связан с выражением ObjectContext, предлагающим свойство internal IQueryProvider Provider { get; }.

Поэтому я создал класс-оболочку (реализующий IQueryProvider), чтобы выполнить перезапись выражения при вызове Execute, а затем передать его исходному провайдеру.

Проблема в том, что поле за Provider равно private ObjectQueryProvider _queryProvider;. Этот ObjectQueryProvider является внутренним запечатанным классом, что означает невозможность создания подкласса, предлагающего добавленную переписку.

Так что этот подход загнал меня в тупик из-за очень тесно связанного ObjectContext.

Как решить эту проблему? Я смотрю в неправильном направлении? Возможно, есть способ ввести себя вокруг этого ObjectQueryProvider?

Обновление: хотя все предоставленные решения работают, когда вы «обертываете» ObjectContext с помощью шаблона репозитория, предпочтительнее было бы решение, позволяющее напрямую использовать сгенерированный подкласс из ObjectContext. Тем самым оставаясь совместимым с каркасом динамических данных.


person Davy Landman    schedule 03.12.2009    source источник


Ответы (3)


На основе ответа Артура я создал рабочую оболочку.

Предоставленные фрагменты позволяют обернуть каждый запрос 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");
}
person Davy Landman    schedule 04.12.2009
comment
У вас есть все, что вам нужно сейчас? Должен ли я предоставить еще несколько вспомогательных методов или поискать что-то в своем коде? - person Arthur; 04.12.2009
comment
Нет, я заставил это работать, но странно, что вы пропустили часть, чтобы исправить это обратно в запрос EF. - person Davy Landman; 05.12.2009

У меня есть именно тот исходный код, который вам нужен, но я не знаю, как прикрепить файл.

Вот несколько фрагментов (фрагменты! Мне пришлось адаптировать этот код, поэтому он может не скомпилироваться):

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

РЕДАКТИРОВАТЬ: добавлены 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&lt;T&gt; 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));
    }
person Arthur    schedule 03.12.2009
comment
Если я не ошибаюсь, мне пришлось бы изменить наборы из сгенерированного подкласса ObjectContext, чтобы использовать эту оболочку вместо вызова base.CreateQuery? Это не совсем хорошее решение, потому что регенерат уничтожает мои изменения? Или я неправильно истолковываю ваш пример использования? - person Davy Landman; 03.12.2009
comment
Привет, не могли бы вы предоставить ExpressionTreeTranslator? Я предполагаю, что это реализация шаблона посетителя Expression Tree? - person Davy Landman; 03.12.2009
comment
@First comment: правильно, вы оборачиваете вызовы CreateQuery. У меня есть собственный генератор, поэтому у меня нет проблем. У меня также есть собственный общий метод GetQuery, который создает правильный запрос EF и упаковывает его. Я опубликую этот метод. @Second: Вы можете найти QueryTranslator здесь: msdn.microsoft.com/en- us/library/bb882521.aspx или здесь blogs.msdn.com/mattwar/archive/2007/07/31/ - person Arthur; 03.12.2009
comment
Итак, это было просто переименование этих классов, вот что я подумал. Но не могли бы вы предоставить метод расширения под названием FindElementTypes()? В гугле тоже не могу найти. - person Davy Landman; 04.12.2009
comment
Извините, но я не могу заставить это работать, поставщику EF не нравится запрос с обернутым вокруг него QueryTranslater. -- System.NotSupportedException: невозможно создать постоянное значение типа «QueryTranslator`1». В этом контексте поддерживаются только примитивные типы ("такие как Int32, String и Guid"). - person Davy Landman; 04.12.2009

Просто хотел добавить к примеру Артура.

Как и предупредил Артур, в его методе 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, вы можете использовать код в моем Совет 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

person Alex James    schedule 03.12.2009
comment
tnx для исправления ошибки;) но, возможно, изменить ссылки на службу сокращения URL-адресов, чтобы сохранить чистоту базы данных StackOverflow? - person Davy Landman; 03.12.2009
comment
Конечно... если бы в моем блоге была хорошая реферальная статистика. - person Alex James; 04.12.2009
comment
О, это я мог понять. Я не знаю, разрешен ли Google Analytics на blogs.msdn.com, но, по моему мнению, они дают хороший обзор ваших посетителей. - person Davy Landman; 04.12.2009