How do I implement an ObjectSet wrapper that works with LinqToEntities?

7

I need to create a wrapper for ObjectSet to be able to create a centralized access control.

The goal is to implement the CA without making changes to queries already in the system, which in this case are scattered throughout the code (there is no centralized layer for data access).

The connection to the bank uses ObjectContext .

The wrapper of ObjectSet is created, this is:

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 is not of type ObjectSet , but this is not important, it only needs to be used in queries LinqToEntities . And it even works for simple queries like this:

//db.Produto é do tipo ObjectSetWrapper<Produto >
var query = (from item in db.Produto where item.Quantidade > 0 select new { item.Id, item.Nome, item.Valor });
var itensList = query.Take(10).ToList();

But when there is a * subquery *, like this:

//db.Produto é do tipo ObjectSetWrapper<Produto>
var query = (from item in db.Produto
             select new
             {
                 Id = item.Id,
                 Nome = item.Nome,
                 QuantidadeVendas = (from venda in db.Venda where venda.IdProduto == item.Id select venda.Id).Count()
             }).OrderByDescending(x => x.QuantidadeVendas);

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

I get a NotSupportedException , stating that it is not possible to create a constant of my subquery entity type (in this case, Venda ):

  

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

How do I make this query work? I do not need the wrapper to be of type ObjectSet , only that it can be used in queries, keeping queries of the system running.

Updated

Update the ObjectSetWrapper class to implement IObjectSet<TEntity> as indicated by Miguel Angelo, but the errors continue. Now the class has this signature:

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

To reinforce, the wrapper idea is to be able to perform access control checks on query queries, so it is important to keep queries with linq to entities already in place on the system.

    
asked by anonymous 06.02.2014 / 13:51

4 answers

0

For those who have the same problem as me, I got the answer I was looking for on stackoverflow.com by Denis Itskovich:

link

    
08.04.2014 / 14:40
3

Possibly you will have to implement the IObjectSet<TEntity> interface so that the entity-framework can know how to work with this object, otherwise the entity will interpret this object as anything that has nothing to do with it.

EDIT

I'll point out a path that will definitely work:

The Expression IQueryable.Expression method returns the expression that will be transformed into SQL by the Entity. If you have this object, which will be obtained from the% original%, you must rebuild it so that the references to ObjectSet are replaced with references of% internal%, in addition to adding a call to the ObjectSetWrapper method to filter according to access control.

Rebuilding ObjectSet is something that will be a lot of work, because it is an immutable AST (abstract syntax tree), that is, you will have to rebuild the entire tree. To do this, you can implement a Where that will convert the original expression:

public class ControleAcessoVisitor : ExpressionVisitor
{
    protected override Expression VisitConstant(ConstantExpression node)
    {
        Expression result = node;

        if (node.Value is ObjectSetWrapper)
        {
            result = Expression.Constant((node.Value as ObjectSetWrapper).inner);

            var whereMethod = typeof(Queryable).GetMethods().Single(m =>
                m.Name == "Where"
                && m.GetParameters().Length == 2
                && m.GetParameters()[1].ParameterType
                    .GetGenericArguments()[1]
                    .GetGenericArguments().Length == 2);

            Expression filtroControleAcesso = PegarExpressionFiltroCA();

            result = Expression.Call(whereMethod, result, filtroControleAcesso);
        }

        return base.Visit(result);
    }
}

I'll leave the implementation of the Expression method for you, this will depend on your specific rules.

And in the% wpper% method of your wraper:

    Expression IQueryable.Expression
    {
        get { return new ControleAcessoVisitor().Visit(this.query.Expression); }
    }

I hope it's worth implementing this ... it's a way to make sure that access control will work, but it's pretty tricky.

    
06.02.2014 / 16:48
1

Extension Methods were designed exactly for this type of problem, where it is not possible or desirable change the base classes. I believe this would be a better solution to your problem than creating a wrapper class . Here is an example:

public static class ObjectSetExtensions
{
    public static ObjectQuery<TEntity> Include(this IQueryable<TEntity> set, string path) where TEntity : EntityObject
    {
        return set.Include(path);
    }

    public void DeleteObject(this IQueryable<TEntity> set, TEntity @object) where TEntity : EntityObject
    {
        set.DeleteObject(@object);
    }

    // etc

}

So any object of type IQueryable<TEntity> "wins" new methods. Ex: meuSet.Include("path") and meuSet.DeleteObject(objeto) . And the original classes continue to be properly recognized by EF.

    
06.02.2014 / 16:57
0

I have the impression that your subquery is not being deferred, so it tries to run along with the main query, which could be causing the error.

Try changing your code this way:

//db.Produto é do tipo ObjectSetWrapper<Produto>
var query = (from item in db.Produto
             select new
             {
                 Id = item.Id,
                 Nome = item.Nome,
                 QuantidadeVendas = db.Venda.Where(m => m.IdProduto == item.Id).ToList().Count() //ToList() para forçar a execução da subquery
             }).OrderByDescending(x => x.QuantidadeVendas);

var productsList = query.Take(10).ToList();
    
06.02.2014 / 14:50