LINQKit icon indicating copy to clipboard operation
LINQKit copied to clipboard

Re-Use static Expressions as filter-rules by table

Open kuebelkasten opened this issue 8 years ago • 3 comments

Given the following data-structure I do have 3 tables.

  • tblUser (holding the users of the application)
  • tblRole (the available roles of the application)
  • tblUserRole (the n-to-m relation between users and the roles they have)

A user can have different states, e.g. Blocked, Active and Inactive.

A tblRole-entity has therefore many tblUser entities bound.

For fetching only user of a certain stati, I've added the following to the partial of tblUser

public static Expression<Func<tblUser, bool>> IsActive => (user) => user.Status == "Active";
public static Expression<Func<tblUser, bool>> IsBlocked => (user) => user.Status == "Blocked";
public static Expression<Func<tblUser, bool>> IsInactive => (user) => user.Status == "Inactive";

Now I can query the data like:

var activeUsers = dbContext.tblUser.AsExpandable().Where(tblUser.IsActive).ToArray();

which will get me all active users actually in my database.

The problem is when doing sub-queries - reusing the same logic for filtering e.g. for active users.

var activeRolesUsers = dbContext.tblRole
    .AsExpandable()
    .Select(role => new
    {
        RoleName = role.RoleName,
        ActiveRoleUsers = role.tblUserRole.Select(userRole => userRole.tblUser).Where(user => tblUser.IsActive.Invoke(user))
    })
    .ToArray();

Which should show me all active users in their respective role. Unfortunately, this throws me the exception:

Unable to cast object of type 'System.Linq.Expressions.PropertyExpression' to type 'System.Linq.Expressions.LambdaExpression'.

File: LinqKit.ExpressionExpander.cs Method: VisitMethodCall Line: 67

For me it would be even better to write something like:

var activeUsers = dbContext.tblUser.AsExpandable().Where(user => tblUser.IsActive.Invoke(user)).ToArray();

which would enable me to have the filterings separated and always called the same way (DRY) and they can be combined by using && and || inside the Where/Single/Any.

Adding the following lines to TransformExpr in starting at line 119:

var prope = input.Member as PropertyInfo;
if (( field != null && field.FieldType.GetTypeInfo().IsSubclassOf(typeof(Expression))) ||
    (prope != null && prope.PropertyType.GetTypeInfo().IsSubclassOf(typeof(Expression))))
    return Visit(Expression.Lambda<Func<Expression>>(input).Compile()());

now it works as expected. This code is taken from http://stackoverflow.com/questions/6226129/is-there-a-particular-reason-linqkits-expander-cant-pick-up-expressions-from-f (the last post).

Looking at the code, this is mainly the same as in TryVisitExpressionFunc (only the check field != null missing).

Is there any specific reason why this should not be done? If so, which one? If not, why not adding it? I know that the code is just a realy dirty hack and I'm willing to know why this feels so and if there are any reasons why it is how it is.

kuebelkasten avatar Jul 26 '16 17:07 kuebelkasten

There is already function TryVisitExpressionFunc doing that. Doesn't it work?

Thorium avatar Aug 03 '16 09:08 Thorium

@Thorium I don't have the code here to check right now. I know about the function but it seems like it is never called at ExpressionExpander.VisitMethodCall line 63 and crashes at line 67 when casting. So my best guess (without looking at the code or debugging) is, that field at line 118 is not null or expression at line 147 is not null.

Maybe there should be a check as in TryVisitExpressionFunc checking if input.Member is PropertyInfo? But this is also another guess as you are the one who have the big picture.

Could you try to reproduce the error/issue?

kuebelkasten avatar Aug 03 '16 12:08 kuebelkasten

I haven't myself used LinqKit in this year at all so I'm not familiar with the latest changes. Back in then, it needed to say var expanded = IsActive.Expand() and then in where clause call expanded.Invoke(user).

You could also do SelectMany (from r in role.tblUserRole.AsExpandable() from u in dbContext.tblUser.AsExpandable()) and then group by role? As what you probably want is to execute just one SQL clause. What you probably not want is to evaluate expressions in select to create separate user queries for each role. Keep SQL profiler running to be sure what is happening.

If there are any problems that needs to be fixed, PRs are usually accepted. :-)

Thorium avatar Aug 03 '16 12:08 Thorium