Обертка ObjectSet не работает с подзапросом linqToEntities

для целей контроля доступа в интенсивной системе БД я должен был реализовать оболочку объектов, где будет проверяться AC.

Основная цель заключается в том, чтобы это изменение сохраняло существующий код для доступа к базе данных, который реализован с помощью linq для сущностей по всем классам (для базы данных нет централизованного уровня).

Созданный ObjectSetWrapper выглядит следующим образом:

public class ObjectSetWrapper<TEntity> : IQueryable<TEntity> where TEntity : EntityObject
{
    private IQueryable<TEntity> QueryableModel;
    private ObjectSet<TEntity> ObjectSet;

    public ObjectSetWrapper(ObjectSet<TEntity> objectSetModels)
    {
        this.QueryableModel = objectSetModels;
        this.ObjectSet = objectSetModels;
    }

    public ObjectQuery<TEntity> Include(string path)
    {
        return this.ObjectSet.Include(path);
    }

    public void DeleteObject(TEntity @object)
    {
        this.ObjectSet.DeleteObject(@object);
    }

    public void AddObject(TEntity @object)
    {
        this.ObjectSet.AddObject(@object);
    }

    public IEnumerator<TEntity> GetEnumerator()
    {
        return QueryableModel.GetEnumerator();
    }

    public Type ElementType
    {
        get { return typeof(TEntity); }
    }

    public System.Linq.Expressions.Expression Expression
    {
        get { return this.QueryableModel.Expression; }
    }

    public IQueryProvider Provider
    {
        get { return this.QueryableModel.Provider; }
    }

    public void Attach(TEntity entity)
    {
        this.ObjectSet.Attach(entity);
    }

    public void Detach(TEntity entity)
    {
        this.ObjectSet.Detach(entity);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.QueryableModel.GetEnumerator();
    }
}

Это очень просто и работает для простых запросов, например:

//db.Product is ObjectSetWrapper<Product>
var query = (from item in db.Product where item.Quantity > 0 select new { item.Id, item.Name, item.Value });
var itensList = query.Take(10).ToList();

Но когда у меня есть такие подзапросы:

//db.Product is ObjectSetWrapper<Product>
var query = (from item in db.Product
             select new
             {
                 Id = item.Id,
                 Name = item.Name,
                 SalesQuantity = (from sale in db.Sale where sale.ProductId == item.Id select sale.Id).Count()
             }).OrderByDescending(x => x.SalesQuantity);

var productsList = query.Take(10).ToList();

Я получаю NotSupportedException, говоря, что я не могу создать постоянное значение моего внутреннего типа сущности запроса:

Невозможно создать постоянное значение типа "MyNamespace.Model.Sale". В этом случае поддерживаются только примитивные типы или типы перечислений. контекст.

Как я могу заставить мои запросы работать? Мне действительно не нужно, чтобы моя обертка была тип ObjectSet, мне просто нужно использовать ее в запросах.


Обновление

Я изменил свою подпись класса. Теперь он также реализует IObjectSet < > , но я получаю то же NotSupportedException:

public class ObjectSetWrapper<TEntity> : IQueryable<TEntity>, IObjectSet<TEntity> where TEntity : EntityObject

Ответ 1

EDIT:

Проблема заключается в том, что следующая конструкция LINQ преобразуется в выражение LINQ, содержащее ваш пользовательский класс внутри (ObjectSetWrapper).

var query = (from item in db.Product
             select new
             {
                 Id = item.Id,
                 Name = item.Name,
                 SalesQuantity = (from sale in db.Sale where sale.ProductId == item.Id select sale.Id).Count()
             }).OrderByDescending(x => x.SalesQuantity);

LINQ to Entities пытается преобразовать это выражение в инструкцию SQL, но не знает, как обращаться с пользовательскими классами (а также с обычными методами).

Решение в таких случаях заключается в замене IQueryProvider на пользовательский, который должен перехватить выполнение запроса и преобразовать выражение LINQ, содержащее пользовательские классы/методы в действительное выражение LINQ to Entities (которое работает с объектами и наборами объектов).

Преобразование выражений выполняется с использованием класса, полученного из ExpressionVisitor, который выполняет обход дерева выражений, заменяя соответствующие узлы, на узлы, которые могут быть приняты LINQ to Entities

Часть 1 - IQueryWrapper

// Query wrapper interface - holds and underlying query
interface IQueryWrapper
{
    IQueryable UnderlyingQueryable { get; }
}

Часть 2 - Реферат QueryWrapperBase (не общий)

abstract class QueryWrapperBase : IQueryProvider, IQueryWrapper
{
    public IQueryable UnderlyingQueryable { get; private set; }

    class ObjectWrapperReplacer : ExpressionVisitor
    {
        public override Expression Visit(Expression node)
        {
            if (node == null || !typeof(IQueryWrapper).IsAssignableFrom(node.Type)) return base.Visit(node);
            var wrapper = EvaluateExpression<IQueryWrapper>(node);
            return Expression.Constant(wrapper.UnderlyingQueryable);
        }

        public static Expression FixExpression(Expression expression)
        {
            var replacer = new ObjectWrapperReplacer();
            return replacer.Visit(expression);
        }

        private T EvaluateExpression<T>(Expression expression)
        {
            if (expression is ConstantExpression) return (T)((ConstantExpression)expression).Value;
            var lambda = Expression.Lambda(expression);
            return (T)lambda.Compile().DynamicInvoke();
        }
    }

    protected QueryWrapperBase(IQueryable underlyingQueryable)
    {
        UnderlyingQueryable = underlyingQueryable;
    }

    public abstract IQueryable<TElement> CreateQuery<TElement>(Expression expression);
    public abstract IQueryable CreateQuery(Expression expression);

    public TResult Execute<TResult>(Expression expression)
    {
        return (TResult)Execute(expression);
    }

    public object Execute(Expression expression)
    {
        expression = ObjectWrapperReplacer.FixExpression(expression);
        return typeof(IQueryable).IsAssignableFrom(expression.Type)
            ? ExecuteQueryable(expression)
            : ExecuteNonQueryable(expression);
    }

    protected object ExecuteNonQueryable(Expression expression)
    {
        return UnderlyingQueryable.Provider.Execute(expression);
    }

    protected IQueryable ExecuteQueryable(Expression expression)
    {
        return UnderlyingQueryable.Provider.CreateQuery(expression);
    }
}

Часть 3 - Общий QueryWrapper<TElement>

class QueryWrapper<TElement> : QueryWrapperBase, IOrderedQueryable<TElement>
{
    private static readonly MethodInfo MethodCreateQueryDef = GetMethodDefinition(q => q.CreateQuery<object>(null));

    public QueryWrapper(IQueryable<TElement> underlyingQueryable) : this(null, underlyingQueryable)
    {
    }

    protected QueryWrapper(Expression expression, IQueryable underlyingQueryable) : base(underlyingQueryable)
    {
        Expression = expression ?? Expression.Constant(this);
    }

    public virtual IEnumerator<TElement> GetEnumerator()
    {
        return ((IEnumerable<TElement>)Execute<IEnumerable>(Expression)).GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    public Expression Expression { get; private set; }

    public Type ElementType
    {
        get { return typeof(TElement); }
    }

    public IQueryProvider Provider
    {
        get { return this; }
    }

    public override IQueryable CreateQuery(Expression expression)
    {
        var expressionType = expression.Type;
        var elementType = expressionType
            .GetInterfaces()
            .Single(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>))
            .GetGenericArguments()
            .Single();

        var createQueryMethod = MethodCreateQueryDef.MakeGenericMethod(elementType);

        return (IQueryable)createQueryMethod.Invoke(this, new object[] { expression });
    }

    public override IQueryable<TNewElement> CreateQuery<TNewElement>(Expression expression)
    {
        return new QueryWrapper<TNewElement>(expression, UnderlyingQueryable);
    }

    private static MethodInfo GetMethodDefinition(Expression<Action<QueryWrapper<TElement>>> methodSelector)
    {
        var methodCallExpression = (MethodCallExpression)methodSelector.Body;
        return methodCallExpression.Method.GetGenericMethodDefinition();
    }
}

Часть 4 - наконец, ваш ObjectSetWrapper

public class ObjectSetWrapper<TEntity> : IQueryable<TEntity>, IQueryWrapper where TEntity : class
{
    private IQueryable<TEntity> QueryableModel;
    private ObjectSet<TEntity> ObjectSet;

    public ObjectSetWrapper(ObjectSet<TEntity> objectSetModels)
    {
        this.QueryableModel = new QueryWrapper<TEntity>(objectSetModels);
        this.ObjectSet = objectSetModels;
    }

    public ObjectQuery<TEntity> Include(string path)
    {
        return this.ObjectSet.Include(path);
    }

    public void DeleteObject(TEntity @object)
    {
        this.ObjectSet.DeleteObject(@object);
    }

    public void AddObject(TEntity @object)
    {
        this.ObjectSet.AddObject(@object);
    }

    public IEnumerator<TEntity> GetEnumerator()
    {
        return QueryableModel.GetEnumerator();
    }

    public Type ElementType
    {
        get { return typeof(TEntity); }
    }

    public System.Linq.Expressions.Expression Expression
    {
        get { return this.QueryableModel.Expression; }
    }

    public IQueryProvider Provider
    {
        get { return this.QueryableModel.Provider; }
    }

    public void Attach(TEntity entity)
    {
        this.ObjectSet.Attach(entity);
    }

    public void Detach(TEntity entity)
    {
        this.ObjectSet.Detach(entity);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.QueryableModel.GetEnumerator();
    }

    IQueryable IQueryWrapper.UnderlyingQueryable
    {
        get { return this.ObjectSet; }
    }
}

Ответ 2

Ваш внутренний запрос завершается с ошибкой, потому что вы ссылаетесь на другой набор данных, когда вы должны перемещаться по внешним ключам:

SalesQuantity = item.Sales.Count()