Jacob Roberts
Jacob Roberts

Reputation: 1815

Linq - How to use Select then Where but Select Entire Entity?

As a stop measure, I'd like developers to get data based on specific criteria but I don't want to expose the entire object for a where clause.

dbContext.BigThing
    .Select(s => new LesserThing { FieldA= s.FieldA, FieldB = s.FieldB })
    .Where(someQueryExpression)
    .TakeTheEntireEntity();

The someQueryExpression will be of type Expression<Func<LesserThing, bool>>. The TakeTheEntireEntity is sudo code for, how do I get the entire data model? I can use the dbContext again as an inner Where clause but this would trigger 2 queries and evaluate client side, which is bad. One trip to the db is required.

The idea here is to allow developers to consume this service but prevent them from querying Where SomeNonIndexedField cannot be used.

Upvotes: 1

Views: 175

Answers (2)

Ivan Stoev
Ivan Stoev

Reputation: 205589

There are many ways to accomplish this (none out-of-the box though), with all they requiring expression tree manipulation.

But if the goal is just to limit the fields available in the Where conditions, there is much simpler approach which works out-of-the-box and requires less coding to apply.

Just make the LesserThing interface and let BiggerThing implement it implicitly. e.g.

public interface ISomeEntityFilter
{
    string FieldA { get; }
    DateTime FieldB { get; }
}

public class SomeEntity : ISomeEntityFilter
{
    public int Id { get; set; }
    public string FieldA { get; set; }
    public DateTime FieldB { get; set; }
    // ... many others
}

Now, given

Expression<Func<ISomeEnityFilter, bool>> filter

coming from the caller, what you do is simply applying it and then casting back to the original type (the latter is needed because the first operation changes the generic type of the result from IQueryable<SomeEntity> to IQueryable<ISomeEntityFilter>, but you know that it sill actually holds SomeEntity elements):

var query = dbContext.Set<SomeEntity>()
    .Where(filter)
    .Cast<SomeEntity>();

And yes (you can easily verify that), the result is server (SQL) translatable EF Core query.

Upvotes: 1

NetMage
NetMage

Reputation: 26917

What you want to do requires working with the Expression tree to transform a lambda of the form pl => pl.prop == x to a new lambda p => p.prop == x.

This code works with either properties or fields, but assumes that the LesserThing will have only member names that also exist in the BigThing. Of course, you could also create a Dictionary and map the LesserThing member names to BigThing member names.

Since you can't infer generic type parameters from the return, you have to manually pass in the types.

var ans = dbContext.BigThing.Where(someQueryExpression.TestBigThing<BigThing,LesserThing>());

Since the type of someQueryExpression must be Expression<Func<LesserThing,bool>> it is only possible to access fields or properties of LesserThing or outside variables or constants.

public static class TestExt {
    public static Expression<Func<TBig, bool>> TestBigThing<TBig, TLesser>(this Expression<Func<TLesser, bool>> pred) {
        // (T p)
        var newParm = Expression.Parameter(typeof(TBig), "p");

        var oldMemberExprs = pred.Body.CollectMemberExpressions();
        var newMemberExprs = oldMemberExprs.Select(m => Expression.PropertyOrField(newParm, m.Member.Name));

        var newBody = pred.Body;
        foreach (var me in oldMemberExprs.Zip(newMemberExprs))
            newBody = newBody.Replace(me.First, me.Second);

        // p => {newBody}
        return Expression.Lambda<Func<TBig,bool>>(newBody, newParm);
    }

    /// <summary>
    /// Collects all the MemberExpressions in an Expression
    /// </summary>
    /// <param name="e">The original Expression.</param>
    /// <returns>List<MemberExpression></returns>
    public static List<MemberExpression> CollectMemberExpressions<T>(this T e) where T : Expression {
        new CollectMemberVisitor().Visit(e);
        return CollectMemberVisitor.MemberExpressions;
    }

    /// <summary>
    /// ExpressionVisitor to collect all MemberExpressions.
    /// </summary>
    public class CollectMemberVisitor : ExpressionVisitor {
        public static List<MemberExpression> MemberExpressions;

        public CollectMemberVisitor() {
            MemberExpressions = new List<MemberExpression>();
        }

        protected override Expression VisitMember(MemberExpression node) {
            MemberExpressions.Add(node);
            return node;
        }
    }
}

Upvotes: 0

Related Questions