Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ObjectSet wrapper not working with linqToEntities subquery

for access control purposes in a intensive DB use system I had to implement an objectset wrapper, where the AC will be checked.

The main objective is make this change preserving the existing code for database access, that is implemented with linq to entities all over the classes (there is no centralized layer for database).

The ObjectSetWrapper created is like that:

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();
    }
}

It's really simple and works for simple queries, like that:

//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();

But when I have subqueries like that:

//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();

I get NotSupportedException, saying I can't create a constant value of my inner query entity type:

Unable to create a constant value of type 'MyNamespace.Model.Sale'. Only primitive types or enumeration types are supported in this context.

How can I get my queries working? I don't really need to make my wrapper an ObjectSet type, I just need to use it in queries.


Updated

I have changed my class signature. Now it's also implementing IObjectSet<>, but I'm getting the same NotSupportedException:

public class ObjectSetWrapper<TEntity> : IQueryable<TEntity>, IObjectSet<TEntity> where TEntity : EntityObject
like image 993
IPValverde Avatar asked Feb 06 '14 12:02

IPValverde


1 Answers

EDIT:

The problem is that the following LINQ construction is translated into LINQ expression containing your custom class inside (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 tries to convert this expression into SQL statement, but it has no idea how to deal with the custom classes (as well as custom methods).

The solution in such cases is to replace IQueryProvider with the custom one, which should intercept the query execution and translate LINQ expression, containing custom classes/methods into valid LINQ to Entities expression (which operates with entities and object sets).

Expression conversion is performed using the class, derived from ExpressionVisitor, which performs expression tree traversal, replacing relevant nodes, to the nodes which can be accepted by LINQ to Entities

Part 1 - IQueryWrapper

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

Part 2 - Abstract QueryWrapperBase (not generic)

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);
    }
}

Part 3 - Generic 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();
    }
}

Part 4 - finally your 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; }
    }
}
like image 194
Denis Itskovich Avatar answered Nov 15 '22 04:11

Denis Itskovich