Reputation: 1815
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
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
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