I'm trying to build a single "Or" predicate from a list of predicates in the form List<Expression<Func<T, bool>>>
public static IQueryable<T> Search<T>(this IQueryable<T> source, List<Expression<Func<T, bool>>> predicates = null)
where T : EntityObject
{
if (predicates == null || predicates.Count == 0)
return source;
else if (predicates.Count == 1)
return source.Where(predicates[0]);
else
{
var row = Expression.Parameter(typeof(T), "row");
var compoundExpression = predicates[0];
for (int i = 1; i < predicates.Count; i++)
{
compoundExpression = compoundExpression.Or(predicates[i]);
}
return source.Where(compoundExpression);
}
}
static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> lhs, Expression<Func<T, bool>> rhs)
{
var row = Expression.Parameter(typeof(T), "row");
var body = Expression.Or(
Expression.Invoke(lhs, row),
Expression.Invoke(rhs, row));
return Expression.Lambda<Func<T, bool>>(body, row);
}
But this is returning every row in my source?
For testing I am looking for c=>c.FullName.Contains("Smith") or c=>c.FullName.Contains("Jones")
I have tried amending to use PredicateBuilder but again it still returns every row in the source.
public static IQueryable<T> Search<T>(this IQueryable<T> source, List<Expression<Func<T, bool>>> predicates = null)
where T : EntityObject
{
if (predicates == null || predicates.Count == 0)
return source;
else if (predicates.Count == 1)
return source.Where(predicates[0]);
else
{
var pb = PredicateBuilder.False<T>();
for (int i = 0; i < predicates.Count; i++)
{
pb = pb.Or(predicates[i]);
}
return source.AsExpandable().Where(pb);
}
}
Any assistance very gratefully received!
The end result would be to allow AND's as well as OR's
e.g. c=>c.FullName.Contains("Dav") AND c=>c.CustomerType == 'Staff'
Try
public static IQueryable<T> Search<T>(this IQueryable<T> source, IEnumerable<Expression<Func<T, bool>>> predicates = null)
where T : EntityObject
{
if (predicates == null || !predicates.Any())
return source;
else
{
ParameterExpression p = Expression.Parameter(typeof(T), "p");
Expression<Func<T,Bool>> predicate =
Expression.Lambda<Func<T,Bool>(
predicates.Select(l => ReParameteriser(l.Body, l.Paramaters[0], p)
.Aggregate((b1,b2) => Expression.Or(b1,b2)),
new ParamaterExpression[]{p});
return source.Where(predicate);
}
}
public class ReParameteriser : ExpressionVisitor
{
ParameterExpression originalParameter;
ParameterExpression newParameter;
private ReParameteriser(){}
protected ReParameteriser (ParameterExpression originalParameter, ParameterExpression newParameter)
{
this.originalParameter = originalParameter;
this.new = newParameter;
}
public static Expression ReParameterise(Expression expression, ParameterExpression originalParameter, ParameterExpression newParameter)
{
return new ReParameteriser(original,newParameter).Visit(expression);
}
protected override Expression VisitParameter(ParameterExpression node)
{
if (node == originalParameter)
return newParameter;
else
return node;
}
}
Note: The ExpressionVisitor class is .Net4 so if you want to target an earlier enviroment you will need to write your own. The code for this is only a google away, but the usual resource is Matt Warren's blog http://blogs.msdn.com/b/mattwar/archive/2008/11/18/linq-links.aspx
Sometimes the weekend does strange things to code!
Not sure what I've done differently, but this is working:
public static IQueryable<T> Search<T>(this IQueryable<T> source, List<Expression<Func<T, bool>>> predicates = null)
where T : EntityObject
{
if (predicates == null || predicates.Count == 0)
return source;
else if (predicates.Count == 1)
return source.Where(predicates[0]);
else
{
var query = PredicateBuilder.False<T>();
foreach (var predicate in predicates)
{
query = query.Or(predicate);
}
return source.AsExpandable().Where(query);
}
}
PredicateBuilder appears to be a great little bit of code.
Related
Im building a filter and im using the FilterByItems method to compare two Arrays(one from my frontend and one from my database). It all goes through a Overlay Panel in my Frontend, where the user can select different persontypes like member, worker and much more. So my problem is now that i got multiple persontype hovers which dont want do work together. If i just have one it works fine, if i add a second one it only works if both have the same values in the frontend list.
The right case would be:
Frontend Panel one got the Member in the Array, Frontend Panel two got the Friend in the Array.
Table shows all persons who are saved with these types in their profile.
The current and wrong case is:
Panel 1 and Panel two are not working with different arrays and only showing informations if both have the same list for example both got member on position [0]
Generelly it seems like it works as a single one as an and query and as the second panel joins in it blocks completely. So here is my code(FilterByItems Method is linked above):
//The Filter
//Login
[HttpPost("filter/")]
public async Task<IActionResult> Filter([FromBody] Filter user)
{
var baseQuery = _context.Personens.AsQueryable();
//Personentyp 1 Dont works in combination with below
if (user.personenTypFilter.Length > 0)
baseQuery = baseQuery.FilterByItems(user.personenTypFilter, (m, k) => m.Personentypzuordnungens.Any(i => i.Personentyp.Bezeichnung.Contains(k)), true);
////Personentyp 2
//if (user.personenTypFilter2.Length > 0)
// baseQuery = baseQuery.FilterByItems(user.personenTypFilter2, (m, k) => m.Personentypzuordnungens.Any(i => i.Personentyp.Bezeichnung.Contains(k)), true);
//---------------------------------
var result = await (baseQuery.Select(p => new
{
personId = p.PersonId,
nachname = p.Nachname,
vorname = p.Vorname,
plz = p.Plz,
firmBez = p.Firmenbezeichnung,
ort = p.Ort,
personentyp = p.Personentypzuordnungens.Select(i => new
{
personentypId = i.PersonentypId,
}),
aktuellePosition = p.AktuellePosition,
taetigkeit = p.Tätigkeit,
kernkompetenzen = p.Kernkompetenzen,
datenReviewedZeitpunkt = p.DatenReviewedZeitpunkt,
}).ToListAsync());
return Ok(result);
}
Thats how i declared my variables in my Filter Model:
public string[] personenTypFilter { get; set; }
public string[] personenTypFilter2 { get; set; }
New problem with .CombineAnd(from comments)
baseQuery = baseQuery.Where(
//Characteristics
character1Predicate.CombineOr(character1Predicate2).CombineOr(character1Predicate3)
//Persontypes
.CombineAnd(personType1Predicate.CombineOr(personType2Predicate).CombineOr(personType3Predicate)));
This function is evolution of FilterByItems and adds additional public methods for generating predicate and combining them.
You can use new extensions in your query in the following way:
var baseQuery = _context.Personens.AsQueryable();
var predicate1 = baseQuery.GetItemsPredicate(ser.personenTypFilter, (m, k) => m.Personentypzuordnungens.Any(i => i.Personentyp.Bezeichnung.Contains(k)));
var predicate2 = baseQuery.GetItemsPredicate(user.personenTypFilter2, (m, k) => m.Personentypzuordnungens.Any(i => i.Personentyp.Bezeichnung.Contains(k)));
// filter by combined predicates
baseQuery = baseQuery.Where(predicate1.CombineOr(predicate2));
And implementation:
public static class QueryableExtensions
{
public static IQueryable<T> FilterByItems<T, TItem>(this IQueryable<T> query, IEnumerable<TItem> items,
Expression<Func<T, TItem, bool>> filterPattern, bool isOr = true, bool emptyValue = true)
{
var filterLambda = query.GetItemsPredicate(items, filterPattern, isOr, emptyValue);
return query.Where(filterLambda);
}
public static Expression<Func<T, bool>> GetItemsPredicate<T, TItem>(this IEnumerable<T> query, IEnumerable<TItem> items, Expression<Func<T, TItem, bool>> filterPattern, bool isOr = true, bool emptyValue = false)
{
Expression predicate = null;
foreach (var item in items)
{
var itemExpr = Expression.Constant(item);
var itemCondition = ExpressionReplacer.Replace(filterPattern.Body, filterPattern.Parameters[1], itemExpr);
if (predicate == null)
predicate = itemCondition;
else
{
predicate = Expression.MakeBinary(isOr ? ExpressionType.OrElse : ExpressionType.AndAlso, predicate,
itemCondition);
}
}
predicate ??= Expression.Constant(emptyValue);
var filterLambda = Expression.Lambda<Func<T, bool>>(predicate, filterPattern.Parameters[0]);
return filterLambda;
}
public static Expression<Func<T, bool>> CombineOr<T>(this Expression<Func<T, bool>> predicate1,
Expression<Func<T, bool>> predicate2)
{
var parameter = predicate1.Parameters[0];
var body = Expression.OrElse(predicate1.Body, ExpressionReplacer.GetBody(predicate2, parameter));
return Expression.Lambda<Func<T, bool>>(body, parameter);
}
public static Expression<Func<T, bool>> CombineAnd<T>(this Expression<Func<T, bool>> predicate1,
Expression<Func<T, bool>> predicate2)
{
var parameter = predicate1.Parameters[0];
var body = Expression.AndAlso(predicate1.Body, ExpressionReplacer.GetBody(predicate2, parameter));
return Expression.Lambda<Func<T, bool>>(body, parameter);
}
class ExpressionReplacer : ExpressionVisitor
{
readonly IDictionary<Expression, Expression> _replaceMap;
public ExpressionReplacer(IDictionary<Expression, Expression> replaceMap)
{
_replaceMap = replaceMap ?? throw new ArgumentNullException(nameof(replaceMap));
}
public override Expression Visit(Expression node)
{
if (node != null && _replaceMap.TryGetValue(node, out var replacement))
return replacement;
return base.Visit(node);
}
public static Expression Replace(Expression expr, Expression toReplace, Expression toExpr)
{
return new ExpressionReplacer(new Dictionary<Expression, Expression> { { toReplace, toExpr } }).Visit(expr);
}
public static Expression Replace(Expression expr, IDictionary<Expression, Expression> replaceMap)
{
return new ExpressionReplacer(replaceMap).Visit(expr);
}
public static Expression GetBody(LambdaExpression lambda, params Expression[] toReplace)
{
if (lambda.Parameters.Count != toReplace.Length)
throw new InvalidOperationException();
return new ExpressionReplacer(Enumerable.Range(0, lambda.Parameters.Count)
.ToDictionary(i => (Expression)lambda.Parameters[i], i => toReplace[i])).Visit(lambda.Body);
}
}
}
I'm creating some extension methods on IQueryable to make wildcard filtering easier. But I'm stumbling into a lot of exceptions when I try to filter a sub list. My example:
public class User {
public string FirstName { get; set; }
public string LastName { get; set; }
public IReadOnlyList<Address> Addresses { get; set; }
...
}
public class Address {
public string Street { get; set; }
...
}
My wildcard implementation speaks for itself, the method expects a list of values and supports StartsWith, EndsWith and Contains. I have a Filter method that looks like this:
public static IQueryable<T> Filter<T>(this IQueryable<T> query, Expression<Func<T, string>> property,
IList<string> values)
{
if (values == null || values.Count == 0)
return query;
Expression<Func<T, bool>> condition;
if (values.Count == 1)
condition = GetBooleanExpressionFromString(property, values.First()).Expand();
else
condition = GetBooleanExpressionFromStringList(property, values).Expand();
return query.Where(condition);
}
And the expressions builders look like:
private static Expression<Func<T, bool>> GetBooleanExpressionFromStringList<T>(
Expression<Func<T, string>> expression,
IList<string> values)
{
var predicate = PredicateBuilder.New<T>();
foreach (var value in values)
{
var parsedValue = value.Replace("*", string.Empty);
if (value.StartsWith("*") && value.EndsWith("*"))
predicate.Or(x => expression.Invoke(x).Contains(parsedValue));
else if (value.StartsWith("*"))
predicate.Or(x => expression.Invoke(x).EndsWith(parsedValue));
else if (value.EndsWith("*"))
predicate.Or(x => expression.Invoke(x).StartsWith(parsedValue));
else predicate.Or(x => expression.Invoke(x) == parsedValue);
}
return predicate;
}
public static Expression<Func<T, bool>> GetBooleanExpressionFromString<T>(
Expression<Func<T, string>> expression,
string value)
{
var parsedValue = value.Replace("*", string.Empty);
if (value.StartsWith("*") && value.EndsWith("*"))
return x => expression.Invoke(x).Contains(parsedValue);
if (value.StartsWith("*"))
return x => expression.Invoke(x).EndsWith(parsedValue);
if (value.EndsWith("*"))
return x => expression.Invoke(x).StartsWith(parsedValue);
return x => expression.Invoke(x) == parsedValue;
}
At the end I can use the Filter method like this:
var query = Session.Query<User>();
query = query.Filter(x => x.FirstName, new [] { "Foo*", "*Bar", "*test*" });
return query;
Now I want to make the same extension for the Street property of the Address list. In normal Linq it would compile to query.Where(x => x.Addresses.Any(y => y.Street.*wildcardstuff*)) and the Filter method would be called like query.Filter(x => x.Addresses, x => x.Street, values). But I'm keep getting NotSupported exceptions. The last thing I tried is this post from EF but its not that typed like i want it to be.
The last implementation I tried is this one:
public static IQueryable<T> FilterList<T, U>(this IQueryable<T> query, Expression<Func<T, IEnumerable<U>>> innerList, Expression<Func<U, string>> property,
IList<string> values)
{
if (values == null || values.Count == 0)
return query;
Expression<Func<U, bool>> condition;
if (values.Count == 1)
condition = GetBooleanExpressionFromString(property, values.First()).Expand();
else
condition = GetBooleanExpressionFromStringList(property, values).Expand();
return query.Where(i => innerList.Invoke(i).Any(j => condition.Invoke(j)));
}
but got this exception: System.NotSupportedException: Cannot parse expression 'x => x.Addresses' as it has an unsupported type. Only query sources (that is, expressions that implement IEnumerable) and query operators can be parsed.
I tried to make an extension on string with the wildcard logic but this throws also a NotSupported exception, anyone has an idea?
I would make it more universal. Which may simplify creating such extensions.
query = query.FilterByWildcard(x => x.FirstName, new [] { "Foo*", "*Bar", "*test*" });
And realization:
public static class QueryExtensions
{
public static IQueryable<T> FilterByWildcard<T>(this IQueryable<T> query, Expression<Func<T, string>> prop, IEnumerable<string> items)
{
return query.FilterBy(items, prop, s =>
{
var pattern = s.Trim('*');
if (s.StartsWith("*"))
if (s.EndsWith("*"))
return e => e.Contains(pattern);
else
return e => e.StartsWith(pattern);
else if (s.EndsWith("*"))
return e => e.EndsWith(pattern);
else
return e => e == s;
});
}
public static IQueryable<T> FilterBy<T, TProp, TItem>(this IQueryable<T> query,
IEnumerable<TItem> items,
Expression<Func<T, TProp>> prop,
Func<TItem, Expression<Func<TProp, bool>>> operationSelector, bool isOr = true)
{
var param = prop.Parameters[0];
Expression predicate = null;
foreach (var item in items)
{
var operation = operationSelector(item);
var body = ExpressionReplacer.GetBody(operation, prop.Body);
if (predicate == null)
{
predicate = body;
}
else
{
predicate = Expression.MakeBinary(isOr ? ExpressionType.OrElse : ExpressionType.AndAlso, predicate,
body);
}
}
if (predicate == null)
return query.Where(e => 1 == 2);
var lambda = Expression.Lambda<Func<T, bool>>(predicate, param);
return query.Where(lambda);
}
class ExpressionReplacer : ExpressionVisitor
{
readonly IDictionary<Expression, Expression> _replaceMap;
public ExpressionReplacer(IDictionary<Expression, Expression> replaceMap)
{
_replaceMap = replaceMap ?? throw new ArgumentNullException(nameof(replaceMap));
}
public override Expression Visit(Expression exp)
{
if (exp != null && _replaceMap.TryGetValue(exp, out var replacement))
return replacement;
return base.Visit(exp);
}
public static Expression Replace(Expression expr, Expression toReplace, Expression toExpr)
{
return new ExpressionReplacer(new Dictionary<Expression, Expression> {{toReplace, toExpr}}).Visit(expr);
}
public static Expression Replace(Expression expr, IDictionary<Expression, Expression> replaceMap)
{
return new ExpressionReplacer(replaceMap).Visit(expr);
}
public static Expression GetBody(LambdaExpression lambda, params Expression[] toReplace)
{
if (lambda.Parameters.Count != toReplace.Length)
throw new InvalidOperationException();
return new ExpressionReplacer(lambda.Parameters.Zip(toReplace)
.ToDictionary(e => (Expression)e.First, e => e.Second)).Visit(lambda.Body);
}
}
}
This answer is fixing my own method but Svyatoslav Danyliv's answer is more futureproof.
public static IQueryable<T> FilterList<T, U>(this IQueryable<T> query, Expression<Func<T, IEnumerable<U>>> innerList, Expression<Func<U, string>> property,
IList<string> values)
{
if (values == null || values.Count == 0)
return query;
Expression<Func<U, bool>> condition;
if (values.Count == 1)
condition = GetBooleanExpressionFromString(property, values.First());
else
condition = GetBooleanExpressionFromStringList(property, values);
Expression<Func<T, bool>> finalCondition = t => innerList.Invoke(t).Any(j => condition.Invoke(j));
return query.Where(finalCondition.Expand());
}
And then use it like this.
var query = Session.Query<User>();
query = query.FilterList(x => x.Addresses, y => y.Street, new [] { "Foo*", "*Bar", "*test*" });
return query;
Which is shorter and cleaner but restricted.
Given an entity framework query, such as
var query = (from property in _dbContext.Properties
join location in _db.Locations
on property.Id equals location.PropertyId
select new PropertyDetail
{
Url = property.Url,
Type = property.Type,
Title = property.Title,
Continent = location.Continent,
Country = location.Country,
State = location.State,
});
I have applied filters such as:
if (!string.IsNullOrWhitespace(searchFilters.Type))
{
query = query.Where(model => model.Type == searchFilters.Type);
}
if (!string.IsNullOrWhitespace(searchFilters.Title))
{
query = query.Where(model => model.Title.Contains(searchFilters.Title));
}
Given the following multi-dimensional array
var locations = new[]
{
new[] {"Africa", "Algeria", ""},
new[] {"Asia", "Hong Kong", ""},
new[] {"Asia", "Singapore", ""},
new[] {"Oceania", "Australia", "New South Wales"},
new[] {"North America", "United States", "California"}
};
How can the "query" be further restricted to only include those entries that match the specified locations {Continent, Country, State(optional)} ?
Unfortunately LINQ to Entities currently does not support joins to inmemory collection, nor Contains for non primitive inmemory collection. The only way I see (actually there is another one described here Entity Framework LINQ Get all items part of another collection, but now I think this is more appropriate) is to construct OR filter using some expression build helper.
For instance, using the PredicateUtils class from Establish a link between two lists in linq to entities where clause, it could be like this:
First, add a little helper method
static Expression<Func<PropertyDetail, bool>> LocationFilter(string value, int index)
{
if (!string.IsNullOrEmpty(value))
{
if (index == 0) return d => d.Continent == value;
if (index == 1) return d => d.Country == value;
if (index == 2) return d => d.State == value;
}
return null;
}
and then use
var locationsFilter = locations.Select(location => location.Select(LocationFilter)
.Aggregate(PredicateUtils.And)).Aggregate(PredicateUtils.Or);
if (locationsFilter != null)
query = query.Where(locationsFilter);
For completeness, here is the helper class used:
public static class PredicateUtils
{
sealed class Predicate<T>
{
public static readonly Expression<Func<T, bool>> True = item => true;
public static readonly Expression<Func<T, bool>> False = item => false;
}
public static Expression<Func<T, bool>> Null<T>() { return null; }
public static Expression<Func<T, bool>> True<T>() { return Predicate<T>.True; }
public static Expression<Func<T, bool>> False<T>() { return Predicate<T>.False; }
public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
{
if (Equals(left, right)) return left;
if (left == null || Equals(left, True<T>())) return right;
if (right == null || Equals(right, True<T>())) return left;
if (Equals(left, False<T>()) || Equals(right, False<T>())) return False<T>();
var body = Expression.AndAlso(left.Body, right.Body.Replace(right.Parameters[0], left.Parameters[0]));
return Expression.Lambda<Func<T, bool>>(body, left.Parameters);
}
public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
{
if (Equals(left, right)) return left;
if (left == null || Equals(left, False<T>())) return right;
if (right == null || Equals(right, False<T>())) return left;
if (Equals(left, True<T>()) || Equals(right, True<T>())) return True<T>();
var body = Expression.OrElse(left.Body, right.Body.Replace(right.Parameters[0], left.Parameters[0]));
return Expression.Lambda<Func<T, bool>>(body, left.Parameters);
}
static Expression Replace(this Expression expression, Expression source, Expression target)
{
return new ExpressionReplacer { Source = source, Target = target }.Visit(expression);
}
class ExpressionReplacer : ExpressionVisitor
{
public Expression Source;
public Expression Target;
public override Expression Visit(Expression node)
{
return node == Source ? Target : base.Visit(node);
}
}
}
UPDATE: As requested in the comments, here is the solution for locations being List<Location>:
var locationsFilter = locations.Select(location =>
{
var filter = PredicateUtils.Null<PropertyDetail>();
if (!string.IsNullOrEmpty(location.Continent))
filter = filter.And(d => d.Continent == location.Continent);
if (!string.IsNullOrEmpty(location.Country))
filter = filter.And(d => d.Country == location.Country);
if (!string.IsNullOrEmpty(location.State))
filter = filter.And(d => d.State == location.State);
return filter;
}).Aggregate(PredicateUtils.Or);
This calls for what is called a correlated subquery in SQL. Assuming they will always occupy the same position, you can use array indexers to access the elements within your locations jagged array.
query = query.Where(model =>
locations.Any(location =>
location[0] == model.Continent &&
location[1] == model.Country &&
(string.IsNullOrEmpty(location[2]) || location[2] == model.State)));
Update: Since LINQ to Entities does not support array indexers, you could convert your jagged array into a collection of anonymous types. (In the long term, it would be preferable to create a class for instantiating your filters. This would be more intuitive than remembering what the elements at each index represent.)
var locationsTyped =
locations.Select(location => new
{
Continent = location[0],
Country = location[1],
State = location[2],
}).ToArray();
query = query.Where(model =>
locationsTyped.Any(location =>
location.Continent == model.Continent &&
location.Country == model.Country &&
(string.IsNullOrEmpty(location.State) || location.State == model.State)));
Is it possible to convert a member epxression together with an object to a method binary expression in c#?
What i've tried so far:
public static void SaveBy<T>(this IDbConnection db, T obj, Expression<Func<T, object>> exp) where T : new()
{
var com = exp.Compile();
if (db.Update(obj, e => exp == com.Invoke(obj)) <= 0)
{
db.Insert(obj);
}
}
public static void UpdateBy<T>(this IDbConnection db, T obj, Expression<Func<T, bool>> exp) where T : new()
{
db.Update(obj, exp);
}
what i am trying to achieve is make a method that can be called with
x.SaveBy(object,model=>model.property)
which will call x.Update, converting a MemberExpression into a methodBinaryExpression like this:
x.Update(object, model=>model.property == object.property);
Half way solution
public static void SaveBy<T>(this IDbConnection db, T obj, Expression<Func<T, object>> exp) where T : new()
{
var result = exp.Compile().Invoke(obj);
var exp2 = Expression.Lambda<Func<T, bool>>(Expression.Equal(exp.Body, Expression.Constant(result)), exp.Parameters);
if (db.Update(obj, exp2) <= 0)
{
db.Insert(obj);
}
}
You can create just new expression
model=>model.property == object.property
public static void SaveBy<T, TProp>(this IDbConnection db, T obj, Expression<Func<T, TProp>> exp) where T : new()
{
var memberExp = (MemberExpression)exp.Body;
var objPropExp = Expression.PropertyOrField(Expression.Constant(obj), memberExp.Member.Name);
var equalExp = Expression.Equal(exp.Body, objPropExp);
var exp2 = Expression.Lambda<Func<T, bool>>(equalExp, exp.Parameters);
//exp2 = {model => (model.prop == value(object).prop)}
if (db.Update(obj, exp2) <= 0)
{
db.Insert(obj);
}
}
I'm getting the following error
The parameter 'p' was not bound in the specified LINQ to Entities
query expression.
I understand the problem (same instance of ParameterExpression should be used with all the expressions in the tree) and have attempted to use solutions I've found online but with no luck.
This is my method
private void SeedEntity<TEntity>(DatabaseContext context, ref TEntity entity, params Expression<Func<TEntity, object>>[] identifierExpressions) where TEntity : class
{
Expression<Func<TEntity, bool>> allExpresions = null;
var parameters = identifierExpressions.SelectMany(x => x.Parameters).GroupBy(x => x.Name).Select(p => p.First()).ToList();
foreach (Expression<Func<TEntity, object>> identifierExpression in identifierExpressions)
{
Func<TEntity, object> vv = identifierExpression.Compile();
object constant = vv(entity);
ConstantExpression constExp = Expression.Constant(constant, typeof(object));
BinaryExpression equalExpression1 = Expression.Equal(identifierExpression.Body, constExp);
Expression<Func<TEntity, bool>> equalExpression2 = Expression.Lambda<Func<TEntity, bool>>(equalExpression1, parameters);
if (allExpresions == null)
{
allExpresions = equalExpression2;
}
else
{
BinaryExpression bin = Expression.And(allExpresions.Body, equalExpression2.Body);
allExpresions = Expression.Lambda<Func<TEntity, bool>>(bin, parameters);
}
}
TEntity existingEntity = null;
if (allExpresions != null)
{
existingEntity = context.Set<TEntity>().FirstOrDefault(allExpresions);
}
if (existingEntity == null)
{
context.Set<TEntity>().Add(entity);
}
else
{
entity = existingEntity;
}
}
It generates an expression for the lookup of an entity based on a number of properties.
It works fine for a single expression, the error only occurs when passing in multiple.
Called like this:
SeedEntity(context, ref e, p=> p.Name);//Works
SeedEntity(context, ref e, p=> p.Name, p=> p.Age);//Fails
It generates something similar to me performing the following:
context.Set<TEntity>().FirstOrDefault(p=>p.Name == e.Name && p.Age == e.Age);
Replacing e.Name && e.Age with a ConstantExpression
You can see in the method above I grab all of the unique params and store them in parameters at the top, then use the same variable throughout.This is the start, but then I need to replace the instances of the parameter in each of the Expression<Func<TEntity, bool>> passed in as the params array, this is where I'm failing.
I've tried enumerate the expressions and use the .Update() method passing in the params
I also tried a solution using the ExpressionVisitor
public class ExpressionSubstitute : ExpressionVisitor
{
public readonly Expression from, to;
public ExpressionSubstitute(Expression from, Expression to)
{
this.from = from;
this.to = to;
}
public override Expression Visit(Expression node)
{
if (node == from) return to;
return base.Visit(node);
}
}
public static class ExpressionSubstituteExtentions
{
public static Expression<Func<TEntity, TReturnType>> RewireLambdaExpression<TEntity, TReturnType>(Expression<Func<TEntity, TReturnType>> expression, ParameterExpression newLambdaParameter)
{
var newExp = new ExpressionSubstitute(expression.Parameters.Single(), newLambdaParameter).Visit(expression);
return (Expression<Func<TEntity, TReturnType>>)newExp;
}
}
You're really close. I don't see the point of your parameters variable. Grouping them by name is a mistake. Why not just pass the parameters from the expression? Then visit if necessary. Your visitor code is fine.
private static void SeedEntity<TEntity>(DbContext context, ref TEntity entity, params Expression<Func<TEntity, object>>[] identifierExpressions)
where TEntity : class
{
Expression<Func<TEntity, bool>> allExpresions = null;
foreach (Expression<Func<TEntity, object>> identifierExpression in identifierExpressions)
{
Func<TEntity, object> vv = identifierExpression.Compile();
object constant = vv(entity);
ConstantExpression constExp = Expression.Constant(constant, typeof(object));
BinaryExpression equalExpression1 = Expression.Equal(identifierExpression.Body, constExp);
Expression<Func<TEntity, bool>> equalExpression2 = Expression.Lambda<Func<TEntity, bool>>(equalExpression1, identifierExpression.Parameters);
if (allExpresions == null)
{
allExpresions = equalExpression2;
}
else
{
var visitor = new ExpressionSubstitute(allExpresions.Parameters[0], identifierExpression.Parameters[0]);
var modifiedAll = (Expression<Func<TEntity,bool>>)visitor.Visit(allExpresions);
BinaryExpression bin = Expression.And(modifiedAll.Body, equalExpression2.Body);
allExpresions = Expression.Lambda<Func<TEntity, bool>>(bin, identifierExpression.Parameters);
}
}
TEntity existingEntity = null;
if (allExpresions != null)
{
existingEntity = context.Set<TEntity>().FirstOrDefault(allExpresions);
}
if (existingEntity == null)
{
context.Set<TEntity>().Add(entity);
}
else
{
entity = existingEntity;
}
}