Entity Framework LINQ Expression on object within object
Asked Answered
C

4

11

Editing this question in the hope to make it clearer.

We have entity framework code first setup. I've simplified two classes for the purposes of example, in reality there are around 10+ more classes similar to the 'Record', where Item is a navigational property/foreign key.

Item class:

public class Item
{
    public int Id { get; set; }
    public int AccountId { get; set; }
    public List<UserItemMapping> UserItemMappings { get; set; }
    public List<GroupItemMapping> GroupItemMappings { get; set; }
}

Record class:

public class Record 
{
    public int ItemId { get; set; }
    public Item Item { get; set; }
}

this.User is an injected user object into each repo and is contained on the repository base. We have an Item repository with the following code:

var items = this.GetAll()
    .Where(i => i.AccountId == this.User.AccountId);

I created the follow expression on the repository base to easily filter on that (in the hope of re-use). We cannot use static extension methods due to how LINQ to entities works (System.NotSupportedException "LINQ to Entities does not recognize the method X and this method cannot be translated into a store expression.").

protected Expression<Func<Item, bool>> ItemIsOnAccount()
{
    return item => item.AccountId == this.User.AccountId;
}

I have solved the case of the above, by doing this:

var items = this.GetAll().Where(this.ItemIsOnAccount());

We have additional filtering based on user permissions within that account (again, another case where I do not want to repeat this code in every repo we have):

protected Expression<Func<Item, bool>> SubUserCanAccessItem()
{
    return item => this.User.AllowAllItems 
        || item.UserItemMappings.Any(d => d.UserId.Value == this.User.Id) 
        || item.GroupItemMappings.Any(vm => 
            vm.Group.GroupUserMappings
                .Any(um => um.UserId == this.User.Id));
}

Which I am able to use as follows:

    var items = this.GetAll().Where(this.SubUserCanAccessItem());

However, what we also need, in the Record repository is a way to solve the following:

var records = this.GetAll()
    .Where(i => i.Item.AccountId == this.User.AccountId);

Because Item is a single navigational property, I do not know how to apply the expressions I have created to this object.

I want to reuse the expression I created in the repo base on all of these other repos, so that my 'permission based' code is all in the same place, but I cannot simply throw it in because the Where clause in this case is of Expression< Func < Record,bool >>.

Creating an interface with a method of:

Item GetItem();

on it and putting it on the Record class does not work because of LINQ to entities.

I cannot also create a base abstract class and inherit from it, because there could be other objects than Item that need to be filtered on. For instance a Record could also have a 'Thing' on it that has permission logic. Not all objects will require to be filtered by 'Item' and 'Thing', some by only one, some by another, some by both:

var items = this.GetAll()
    .Where(this.ItemIsOnAccount())
    .Where(this.ThingIsOnAccount());

var itemType2s = this.GetAll().Where(this.ThingIsOnAccount());

var itemType3s = this.GetAll().Where(this.ItemIsOnAccount());

Due to this having a single parent class would not work.

Is there a way in which I can reuse the expressions I have already created, or at least create an expression/modify the originals to work across the board within the OTHER repos that of course return their own objects in a GetAll, but all have a navigation property to Item? How would I need to modify the other repos to work with these?

Thanks

Celestyn answered 15/3, 2019 at 17:2 Comment(1)
Have you tried to use .AsQueriable(); that way you can add additional filters as you like.Costermansville
A
15

The first step for expression reusability is to move the expressions to a common static class. Since in your case they are tied to User, I would make them User extension methods (but note that they will return expressions):

public static partial class UserFilters
{
    public static Expression<Func<Item, bool>> OwnsItem(this User user)
        => item => item.AccountId == user.AccountId;

    public static Expression<Func<Item, bool>> CanAccessItem(this User user)
    {
        if (user.AllowAllItems) return item => true;
        return item => item.UserItemMappings.Any(d => d.UserId.Value == user.Id) ||
            item.GroupItemMappings.Any(vm => vm.Group.GroupUserMappings.Any(um => um.UserId == user.Id));
    }
}

Now the Item repository would use

var items = this.GetAll().Where(this.User.OwnsItem());

or

var items = this.GetAll().Where(this.User.CanAccessItem());

In order to be reusable for entities having Item reference, you would need a small helper utility for composing lambda expressions from other lambda expressions, similar to Convert Linq expression "obj => obj.Prop" into "parent => parent.obj.Prop".

It's possible to implement it with Expression.Invoke, but since not all query providers support for invocation expressions (EF6 doesn't for sure, EF Core does), as usual we'll use a custom expression visitor for replacing a lambda parameter expression with another arbitrary expression:

public static partial class ExpressionUtils
{
    public static Expression ReplaceParameter(this Expression expression, ParameterExpression source, Expression target)
        => new ParameterReplacer { Source = source, Target = target }.Visit(expression);

    class ParameterReplacer : ExpressionVisitor
    {
        public ParameterExpression Source;
        public Expression Target;
        protected override Expression VisitParameter(ParameterExpression node)
            => node == Source ? Target : node;
    }
}

And the two composing functions are as follows (I don't like the name Compose, so sometimes I use the name Map, sometimes Select, Bind, Transform etc., but functionally they do the same. In this case I'm using Apply and ApplyTo, with the only difference being the transformation direction):

public static partial class ExpressionUtils
{
    public static Expression<Func<TOuter, TResult>> Apply<TOuter, TInner, TResult>(this Expression<Func<TOuter, TInner>> outer, Expression<Func<TInner, TResult>> inner)
        => Expression.Lambda<Func<TOuter, TResult>>(inner.Body.ReplaceParameter(inner.Parameters[0], outer.Body), outer.Parameters);

    public static Expression<Func<TOuter, TResult>> ApplyTo<TOuter, TInner, TResult>(this Expression<Func<TInner, TResult>> inner, Expression<Func<TOuter, TInner>> outer)
        => outer.Apply(inner);
}

(Nothing special there, code provided for completeness)

Now you could reuse the original filters by "applying" them to a expression which selects Item property from another entity:

public static partial class UserFilters
{
    public static Expression<Func<T, bool>> Owns<T>(this User user, Expression<Func<T, Item>> item)
        => user.OwnsItem().ApplyTo(item);

    public static Expression<Func<T, bool>> CanAccess<T>(this User user, Expression<Func<T, Item>> item)
        => user.CanAccessItem().ApplyTo(item);
}

and add the following to the entity repository (in this case, Record repository):

static Expression<Func<Record, Item>> RecordItem => entity => entity.Item;

which would allow you to use there

var records = this.GetAll().Where(this.User.Owns(RecordItem));

or

var records = this.GetAll().Where(this.User.CanAccess(RecordItem));

This should be enough to satisfy your requirements.


You can go further and define an interface like this

public interface IHasItem
{
    Item Item { get; set; }
}

and let the entities implement it

public class Record : IHasItem // <--
{
    // Same as in the example - IHasItem.Item is auto implemented
    // ... 
}

then add additional helpers like this

public static partial class UserFilters
{
    public static Expression<Func<T, Item>> GetItem<T>() where T : class, IHasItem
        => entity => entity.Item;

    public static Expression<Func<T, bool>> OwnsItem<T>(this User user) where T : class, IHasItem
        => user.Owns(GetItem<T>());

    public static Expression<Func<T, bool>> CanAccessItem<T>(this User user) where T : class, IHasItem
        => user.CanAccess(GetItem<T>());
}

which would allow you omit the RecordItem expression in the repository and use this instead

var records = this.GetAll().Where(this.User.OwnsItem<Record>());

or

var records = this.GetAll().Where(this.User.CanAccessItem<Record>());

Not sure if it gives you a better readability, but is an option, and syntactically is closer to Item methods.

For Thing etc. just add similar UserFilters methods.


As a bonus, you can go even further and add the usual PredicateBuilder methods And and Or

public static partial class ExpressionUtils
{
    public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
        => Expression.Lambda<Func<T, bool>>(Expression.AndAlso(left.Body,
            right.Body.ReplaceParameter(right.Parameters[0], left.Parameters[0])), left.Parameters);

    public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
        => Expression.Lambda<Func<T, bool>>(Expression.OrElse(left.Body,
            right.Body.ReplaceParameter(right.Parameters[0], left.Parameters[0])), left.Parameters);
}

so you could use something like this if needed

var items = this.GetAll().Where(this.User.OwnsItem().Or(this.User.CanAccessItem()));

in the Item repository, or

var records = this.GetAll().Where(this.User.OwnsItem<Record>().Or(this.User.CanAccessItem<Record>()));

in the Record repository.

Ambros answered 29/3, 2019 at 21:55 Comment(6)
I have unfortunately been very busy so unable to check this out. I will award you the bounty simply due to the incredibly complex and in depth answer you've given, much appreciated. Hopefully I can come back around and re-evaluate this in the next week or so.Celestyn
Hi Ivan, there are no DM functions here but you were fantastic on this question and was wondering if you would have a second to have a look at #59582316 - I am not great with expressions and this previous answer of your's has helped me a huge deal and I'm most appreciative of it.Celestyn
Hi @Seb, This one is not so simple. Worth trying some 3rd party expression composing helper library, for instance LinqKit Invoke / Expand. Otherwise you basically have to implement something similar to what is explained there. Or NeinLinq.Ambros
Hi @Ivan thanks for the tip. I will take a look into it. Am simply trying to avoid writing 2 mapping expressions for the sake of reusability!Celestyn
Thanks for this Ivan, I solved it using your advice to look at LinqKit. #59582316Celestyn
Wow. I am really amazed at the extension method on the Expression. Maybe you could use this to map POCOs to VMs like Expression<Func<Item, ItemVM>> AsVM(this Item item) => new ItemVM { prop = item.prop, ...} then maybe _db.Item.AsVM(). Still not sure on the syntax exactly, but it's a path for sure.Andalusia
W
2

I can't really tell if this could work in your case, depends on how your entities might be setup, but one thing you can try is to have an interface like IHasItemProperty with a GetItem() method and have the entities where you want to use this implement that interface. Something like this :

public interface IHasItemProperty {
    Item GetItem();
}

public class Item: IHasItemProperty {

    public Item GetItem() {
       return this;
    }

    public int UserId {get; set;}
}

public class Record: IHasItemProperty {
    public Item item{get;set;}

    public Item GetItem() {
        return this.item;
    }
}

public class Repo
{
    protected Expression<Func<T, bool>> ItemIsOnAccount<T>() where T: IHasItemProperty
    {
        return entity => entity.GetItem().UserId == 5;
    }

}

I have used an int just to make things simpler.

Walli answered 16/3, 2019 at 20:48 Comment(2)
Interestingly I was trying to come up with a way of doing this via inheritance or interfaces (however via inheritance I would be locked to only inherit one class that had 'Item' as a property, and thus preclude me from having an Item2, for instance, and couldn't think of how to do it with interfaces!), I will give this a go thanks!Celestyn
Alas it was not meant to be:System.NotSupportedException LINQ to Entities does not recognize the method 'DataLayer.Models.Item GetItem()' method, and this method cannot be translated into a store expression.Celestyn
E
0

You should be able to do this with .AsQueryable().

class Account
{
    public IEnumerable<User> Users { get; set; }
    public User SingleUser { get; set; }


    static void Query()
    {
        IQueryable<Account> accounts = new Account[0].AsQueryable();

        Expression<Func<User, bool>> userExpression = x => x.Selected;

        Expression<Func<Account, bool>> accountAndUsersExpression =
            x => x.Users.AsQueryable().Where(userExpression).Any();
        var resultWithUsers = accounts.Where(accountAndUsersExpression);

        Expression<Func<Account, bool>> accountAndSingleUserExpression =
            x => new[] { x.SingleUser }.AsQueryable().Where(userExpression).Any();
        var resultWithSingleUser = accounts.Where(accountAndSingleUserExpression);
    }
}

class User
{
    public bool Selected { get; set; }
}
Elizebethelizondo answered 5/4, 2019 at 6:31 Comment(0)
L
-2

You should only use sql (or your database like) items for the predicate. If you put this.User.AccountId into your lambda, that does not exists at database and can't be parsed by it, that's the source of your error message.

Labile answered 15/3, 2019 at 19:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.