I'm trying to create a complex Linq query that goes like this:
Get all organisations which have employees that match the given filter parameters.
Example filter:
Firstname: John
Name: Smith
My first attempt:
if (!filter.Name.IsNullOrWhiteSpace())
{
query = query.Where(o => o.Persons.Any(p => p.Name.ToLower().Contains(filter.Name.ToLower())));
}
if (!filter.Firstname.IsNullOrWhiteSpace())
{
query = query.Where(o => o.Persons.Any(p => p.Firstname.ToLower().Contains(filter.Firstname.ToLower())));
}
if (!filter.ContactNumber.IsNullOrWhiteSpace())
{
query = query.Where(o => o.Persons.Any(p => p.ContactNumber.contains(filter.ContactNumber)));
}
The problem with this approach is that when there is someone with the firstname John (ex. John Johnson) in organisation A, and someone with the last name Smith (Jenny Smith) in organisation A. The organisation (A) that contains those two persons gets returned. Which it shouldn't. I only want organisations that have people with the firstname "john" AND the lastname "Smith"
I found a working, but dirty and non-scalable approach:
if (!filter.ContactNumber.IsNullOrWhiteSpace())
{
if (!filter.Name.IsNullOrWhiteSpace() && !filter.Firstname.IsNullOrWhiteSpace())
{
query = query.Where(o => o.Persons.Any(p => p.ContactNumber.contains(filter.ContactNumber)
&& p.Name.ToLower().Contains(filter.Name.ToLower())
&& p.Firstname.ToLower().Contains(filter.Firstname.ToLower())));
}
else if (!filter.Name.IsNullOrWhiteSpace())
{
query = query.Where(o => o.Persons.Any(p => p.ContactNumber.contains(filter.ContactNumber)
&& p.Name.ToLower().Contains(filter.Name.ToLower())));
} else if (!filter.Firstname.IsNullOrWhiteSpace())
{
query = query.Where(o => o.Persons.Any(p => p.ContactNumber.contains(filter.ContactNumber)
&& p.Firstname.ToLower().Contains(filter.Firstname.ToLower())));
} else
{
query = query.Where(o => o.Persons.Any(p => p.ContactNumber.contains(filter.ContactNumber));
}
} else if(!filter.Name.IsNullOrWhiteSpace())
{
if (!filter.Firstname.IsNullOrWhiteSpace())
{
query = query.Where(o => o.Persons.Any(p => p.Firstname.ToLower().Contains(filter.Firstname.ToLower()) && p.Name.ToLower().Contains(filter.Name.ToLower())));
} else
{
query = query.Where(o => o.Persons.Any(p => p.Name.ToLower().Contains(filter.Name.ToLower())));
}
} else if (!filter.Firstname.IsNullOrWhiteSpace())
{
query = query.Where(o => o.Persons.Any(p => p.Firstname.ToLower().Contains(filter.Firstname.ToLower())));
}
As you can see this not a very clean solution.
I also tried using method calls inside the expression but Linq couldnt translate that. Is there any way I can can make a list of predicate expressions an merge them to one? Or is there a another, better solution?
By the way, since I need a paginated list, it all has to be in one query.
For your information, this is what my filter class looks like. It is just a class send from my front-end with all the fields that need to be filtered.
public class ContactFilter
{
public string Name{ get; set; }
public string Firstname{ get; set; }
public string ContactNummer { get; set; }
}
One of the easiest solution is using LINQKit library:
var predicate = PredicateBuilder.New<Person>();
if (!filter.Name.IsNullOrWhiteSpace())
{
predicate = predicate.And(p => p.Name.ToLower().Contains(filter.Name.ToLower()));
}
if (!filter.Firstname.IsNullOrWhiteSpace())
{
predicate = predicate.And(p => p.Firstname.ToLower().Contains(filter.Firstname.ToLower()));
}
if (!filter.ContactNumber.IsNullOrWhiteSpace())
{
predicate = predicate.And(p => p.ContactNumber.contains(filter.ContactNumber));
}
Expression<Func<Person, bool>> exp = predicate;
query = query
.AsExpandable()
.Where(o => o.Persons.Any(exp.Compile()));
Is there any way I can can make a list of predicate expressions an merge them to one?
Yes, and that's the approach I'd prefer in this situation.
First build the list:
var filterExpressions = new List<Expression<Func<Person, bool>>();
if (!filter.Name.IsNullOrWhiteSpace())
{
filterExpressions.Add(p => p.Name.ToLower().Contains(filter.Name.ToLower()));
}
if (!filter.Firstname.IsNullOrWhiteSpace())
{
filterExpressions.Add(p => p.Firstname.ToLower().Contains(filter.Firstname.ToLower()));
}
if (!filter.ContactNumber.IsNullOrWhiteSpace())
{
filterExpressions.Add(p => p.ContactNumber.contains(filter.ContactNumber));
}
From there, you can use this implementation to And arbitrary Expressions together. You'll also need to decide what to do if there are no filters to apply (I'll use a default of no filter, but you may want to do something else).
var predicate = filterExpressions.DefaultIfEmpty(p => true)
.Aggregate((a, b) => a.And(b));
Now we get to the hard part. We have an expression that represents the lambda you want to pass to a call to Any. It would be nice if we could just do:
query = query.Where(o => o.Persons.Any(predicate));
But sadly, this won't work because the type of o.Persons isn't an IQueryable. So now we have an expression that we want to embed in another expression in which the inner expression needs to be a lambda. Fortunately this isn't too complicated:
public static Expression<Func<TSource, TResult>> EmbedLambda
<TSource, TResult, TFunc1, TFunc2>(
this Expression<Func<TFunc1, TFunc2>> lambda,
Expression<Func<TSource, Func<TFunc1, TFunc2>, TResult>> expression)
{
var body = expression.Body.Replace(
expression.Parameters[1],
lambda);
return Expression.Lambda<Func<TSource, TResult>>(
body, expression.Parameters[0]);
}
(Using a helper class from the above link)
Now we just need to call the method. Note we won't be able to rely entirely on type inference due to the way this all works out, so some types need to be specified explicitly.
query = query.Where(predicate.EmbedLambda((UnknownType o, Func<Person, bool> p) => o.Persons.Any(p)));
Related
I'm new to using Moq and I'm trying to get the value passed into a Moq'd method to use in the Returns method.
I was doing the following with success.
_repositoryMock.Setup(x => x.GetByOrderId(It.IsAny<string>()))
.Returns((string id) => Task.FromResult(
new Order
{
Id = id
}));
Usage in code:
var order = _repository.GetByOrderId("123");
The above worked fine and the id passed into the Returns method is the same ID I passed into the GetByOrderId method in my code.
However, I would like to make my repository more generic so I want to change my GetByOrderId to FindFirstOrDefault that takes an expression predicate instead of an ID.
Usage like this:
var order = _repository.FindFirstOrDefault( o => x.Id == "123");
With unit test changed to this:
_repositoryMock.Setup(moq => moq.FindFirst(It.IsAny<Expression<Func<Order, bool>>>()))
.Returns((Expression<Func<Order, bool>> expression) => Task.FromResult(
new Order
{
Id = // ... HOW TO GET ID LIKE THE FIRST SAMPLE?
}));
So how can I get to that ID? The "123". Is there any way?
I found a work around.
I just set up a list of Order that had all the values I knew would be in my expected result an then I applied the expression to that list to get the item I wanted.
var expected = // INIT MY EXPECTED
List<Order> orders = new List<Order>();
foreach (var expectedItem in expected.Items)
{
orders.Add(new Order
{
Id = expectedItem.Id,
});
}
Then I setup my Mock like this.
_finicityRepositoryMock.Setup(moq => moq.FindFirst(It.IsAny<Expression<Func<Order, bool>>>()))
.Returns((Expression<Func<Order, bool>> expression) =>
{
var order = orders.AsQueryable().Where(expression).FirstOrDefault();
if (order == null)
{
return Task.FromResult((Order)null);
}
return Task.FromResult(
new Order
{
BorrowerID = order.Id
});
});
You could analyze the Expression.
If you only do x => x.Id == "123" in the predicate expression, the solution could be as simple as:
mock.Setup(x => x.FindFirstOrDefault(It.IsAny<Expression<Func<Order, bool>>>()))
.Returns<Expression<Func<Order, bool>>>(
predicate =>
{
var id = (string)((ConstantExpression)(((BinaryExpression)predicate.Body).Right)).Value;
return new Order { Id = id };
});
If you also use other properties, then you need an ExpressionVisitor which helps you extract values for each property:
class PredicatePropertyValueExpressionVisitor : ExpressionVisitor
{
public Dictionary<string, object> PropertyValues { get; } = new Dictionary<string, object>();
protected override Expression VisitBinary(BinaryExpression node)
{
if (node.Left is MemberExpression pe && pe.Member is PropertyInfo pi)
{
PropertyValues[pi.Name] = (node.Right as ConstantExpression).Value;
}
return base.VisitBinary(node);
}
}
Then mock will be:
mock.Setup(x => x.FindFirstOrDefault(It.IsAny<Expression<Func<Order, bool>>>()))
.Returns<Expression<Func<Order, bool>>>(
predicate =>
{
var visitor = new PredicatePropertyValueExpressionVisitor();
visitor.Visit(predicate);
return new Order { Id = visitor.PropertyValues["Id"].ToString() };
});
I have a method on the back end, that gets values related to a foreign key of the table.
Those foreign keys can be nullable, but one of those keys always will have value.
Here is method
public async Task<ListResultDto<QuoteListDto>> GeQuotesTabData(int? landlordId, int? agentId,
int? propertyTenantId)
{
if (landlordId.HasValue)
{
var query = _quoteRepository.GetAll()
.Where(x => x.LandlordId == landlordId);
}
if (agentId.HasValue)
{
var query = _quoteRepository.GetAll()
.Where(x => x.AgentId == agentId);
}
if (propertyTenantId.HasValue)
{
var query = _quoteRepository.GetAll()
.Where(x => x.PropertyTenantId == propertyTenantId);
}
return new ListResultDto<QuoteListDto>(await query.ProjectTo<QuoteListDto>(ObjectMapper)
.OrderBy(x => x.Id).ToListAsync());
}
At this row, I get an error Cannot resolve symbol query
return new ListResultDto<QuoteListDto>(await query.ProjectTo<QuoteListDto>(ObjectMapper)
.OrderBy(x => x.Id).ToListAsync());
How do I need to rewrite my method?
Declare and initialise your variable. Additionally I would re-write you method like so:
public async Task<ListResultDto<QuoteListDto>> GeQuotesTabData(int? landlordId, int? agentId,
int? propertyTenantId)
{
var query = _quoteRepository.GetAll();
if (landlordId.HasValue)
{
query = query.Where(x => x.LandlordId == landlordId);
}
if (agentId.HasValue)
{
query = query.Where(x => x.AgentId == agentId);
}
if (propertyTenantId.HasValue)
{
query = query .Where(x => x.PropertyTenantId == propertyTenantId);
}
return new ListResultDto<QuoteListDto>(await query.ProjectTo<QuoteListDto>(ObjectMapper)
.OrderBy(x => x.Id).ToListAsync());
}
Also taken from this answer, you can create a WhereIf extension to clean up the if statements.
public static IQueryable<TSource> WhereIf<TSource>(
this IQueryable<TSource> source,
bool condition,
Expression<Func<TSource, bool>> predicate)
{
if (condition)
return source.Where(predicate);
else
return source;
}
Making your code look like this:
public async Task<ListResultDto<QuoteListDto>> GeQuotesTabData(int? landlordId, int? agentId,
int? propertyTenantId)
{
var list = await _quoteRepository.GetAll()
.WhereIf(landlordId.HasValue, x => x.LandlordId == landlordId)
.WhereIf(agentId.HasValue, x => x.AgentId == agentId)
.WhereIf(propertyTenantId.HasValue, x => x.PropertyTenantId == propertyTenantId)
.ProjectTo<QuoteListDto>(ObjectMapper)
.OrderBy(x => x.Id)
.ToListAsync();
return new ListResultDto<QuoteListDto>(list);
}
Your problem is variable scope. When you define a variable it is only visible in the scope you define it in.
You define three different query variables in a local scope. None of them are accessible where you try to use it.
You need to define it before using it, something like this:
public async Task<ListResultDto<QuoteListDto>> GeQuotesTabData(int? landlordId, int? agentId,
int? propertyTenantId)
{
IQueryable<Quote> query = null;
if (landlordId.HasValue)
{
query = _quoteRepository.GetAll().Where(x => x.LandlordId == landlordId);
}
if (agentId.HasValue)
{
query = _quoteRepository.GetAll().Where(x => x.AgentId == agentId);
}
if (propertyTenantId.HasValue)
{
query = _quoteRepository.GetAll().Where(x => x.PropertyTenantId == propertyTenantId);
}
return new ListResultDto<QuoteListDto>(await query.ProjectTo<QuoteListDto>(ObjectMapper)
.OrderBy(x => x.Id).ToListAsync());
}
Of course all of your queries should be of the same type. Otherwise you will have to define and execute them in the local scopes.
You should probably also add some error handling of the case where query is null, when you try to use it.
I have an application written using C# on the top of the ASP.NET MVC 5 framework. In addition, I am using EntityFramework 6.2 as an ORM to interact with my data.
I wrote the following join statement using Fluent LINQ
List<string> names = new List<string>{"", ....};
query = TopLevelQuery.Where(x => x.EndedAt.HasValue && x.StartedAt >= startedAt && x.EndedAt.Value <= endedAt)
.Join(UserService.QueryUniqueRecords(),
entry => entry.UserId,
rec => rec.UserId,
(entry, rec) => new { entry, rec })
.Where(result => result.entry.IsEqualDateOf(result.rec.DayOf)
&& names.Contains(result.rec.Name))
.Select(x => x.entry);
However, I get the following error during runtime
LINQ to Entities does not recognize the method 'IsEqualDateOf', and
this method cannot be translated into a store expression.
Here is my IsEqualDateOf extension method
public static class MyModelExtensions
{
public static DateTime GetLocalDate(this MyModel entry)
{
var local = DbFunctions.TruncateTime(SqlFunctions.DateAdd("ss", entry.UtcOffset, entry.StartedAt));
return local.Value;
}
public static bool IsEqualDateOf(this MyModel entry, DateTime dateOf)
{
bool isEqual = entry.GetLocalDate().Equals(dateOf.Date);
return isEqual;
}
}
However, if I convert my LINQ expression to the following pseudo, it works as expected
query = TopLevelQuery.Where(x => x.EndedAt.HasValue && x.StartedAt >= startedAt && x.EndedAt.Value <= endedAt)
.Join(UserService.QueryUniqueRecords(),
entry => entry.UserId,
rec => rec.UserId,
(entry, rec) => new { entry, rec })
.Where(result => DbFunctions.TruncateTime(SqlFunctions.DateAdd("ss", entry.UtcOffset, entry.StartedAt)) == result.rec.DayOf
&& names.Contains(result.rec.Name))
.Select(x => x.entry);
But, I want to be able to reuse the same logic in multiple places within my project, which is why I want to extract it into some kind of extension or method.
How, can I extract the DbFunctions and SqlFunctions call into a reusable method in which can be used in LINQ before AsEnumerable() is called?
UPDATED
I also tried to extract the logic into lambda expression by adding the following code to MyModel class
public class MyModel
{
public DateTime StartedAt { get; set; }
public int UtcOffset { get; set; }
// ...
public Expression<Func<MyModel, bool>> IsDateOf(DateTime dayOf)
{
return p => p.StartedAt.AddSeconds(p.UtcOffset) == dayOf.Date;
}
}
Then I tried to consume it like so
query = TopLevelQuery.Where(x => x.EndedAt.HasValue && x.StartedAt >= startedAt && x.EndedAt.Value <= endedAt)
.Join(UserService.QueryUniqueRecords(),
entry => entry.UserId,
rec => rec.UserId,
(entry, rec) => new { entry, rec })
.Where(result => result.entry.IsDateOf(result.rec.DayOf)
&& names.Contains(result.rec.Name))
.Select(x => x.entry);
But that throws the following syntax error when trying to consume it
Operator '&&' cannot be applied to operands of type
'Expression<Func<MyModel,bool>>' and bool.
Moreover, I tried making the IsDateOf expression static and called it like so
query = TopLevelQuery.Where(x => x.EndedAt.HasValue && x.StartedAt >= startedAt && x.EndedAt.Value <= endedAt)
.Join(UserService.QueryUniqueRecords(),
entry => entry.UserId,
rec => rec.UserId,
(entry, rec) => new { entry, rec })
.Where(result => result.entry.Where(MyModel.IsDateOf(result.rec.DayOf))
&& names.Contains(result.rec.Name))
.Select(x => x.entry);
But that gives me the following syntax error
'MyMode' does not contain a definition for Where and the best
extension method overload
Queryable.Where<MyModel>IQueryable<MyModel>, Expression<Func<MyModel,
bool>>) required a reciever of type IQueryable
Remove the parameter from your method and make the expression accept two parameters:
public static Expression<Func<MyModel, DateTime, bool>> IsDateOf
= (MyModel p, DateTime d) => p.StartedAt.AddSeconds(p.UtcOffset) == d.Date;
Note I have made it a static field, not a method. Then in your LINQ expression, you need to invoke it:
MyModel.IsDateOf(result.entry, result.rec.DayOf)
If you don't make it a field (or property), you have to first invoke the method to get the expression; then you need to invoke the expression:
MyModel.IsDateOf()(result.entry, result.rec.DayOf)
Which, in my opinion, looks weird.
I have a model that looks like the following:
public class MyType{
public string Id {get;set;}
public string Name{get;set;}
public List<MyType> Children{get;set;}
}
and in my data I have just two level data, meaning my objects will look like:
{
MyType{"1","firstParent",
{
MyType{"2","firstChild",null},
MyType{"3","secondChild",null}}
},
MyType{"4","secondParent",
{
MyType{"5","firstChild",null},
MyType{"6","secondChild",null}}
}
}
How do I query to get MyType object with a specific Id where it might be a parent or child?
The following will return only parents.
collection.FirstOrDefault(c => c.id==id)
You can use Any with a recursive local function to find objects on any level (your data structure would seem to indicate a deeper level is possible)
bool hasIdOrChildren(MyType t, string localId)
{
return t.Id == localId || (t.Children != null && t.Children.Any(o => hasIdOrChildren(o, localId)));
}
collection.FirstOrDefault(c => hasIdOrChildren(c, id));
Or using pre C#7 syntax:
Func<MyType, string, bool> hasIdOrChildren = null;
hasIdOrChildren = (MyType t, string localId) =>
{
return t.Id == localId || (t.Children != null && t.Children.Any(o => hasIdOrChildren(o, localId)));
};
collection.FirstOrDefault(c => hasIdOrChildren(c, id));
If you are only interested in one level, you can drop the reclusiveness:
collection.FirstOrDefault(c => c.Id == id || (c.Children != null && c.Children.Any(o => o.Id == id)));
Edit
The code above gives the parent if any child has the id, you can also flatten the whole tree structure using SelectMany also with a recursive function:
IEnumerable<MyType> flattenTree(MyType t)
{
if(t.Children == null)
{
return new[] { t };
}
return new[] { t }
.Concat(t.Children.SelectMany(flattenTree));
};
collection
.SelectMany(flattenTree)
.FirstOrDefault(c => c.Id == id);
This method can be useful for any type of processing where you need to flatten the tree.
You could build a list of all MyType including children and then query on it like this :
collection.SelectMany(c => c.Children).Concat(collection).Where(c => c.id == id)
I think you're looking for
var flattenedList = IEnumerable.SelectMany(i => i.ItemsInList);
This flattens the list and gives back one list with all items in it.
In your case you need to select
collection.SelectMany(c => c.Type).Concat(collection).Where(item => item.Id == 5);
MSDN
You still got the childs in your joined parents here, but you can still erase them or ignore them.
I think, you should flatten collection using SelectMany method, then use FirstOrDefault to get element by id:
MyType selected = collection
.SelectMany(obj => new MyType[] {obj, obj.NestedList})
.FirstOrDefault(obj => obj.id == id);
I'm applying a .Where()-restriction on an IQueryOver<T,T> in FluentNH, as such:
.Where(x => x.Col1 == null || x.Col1 == "");
Which generates the following SQL:
WHERE (Col1 IS NULL OR Col1 = NULL)
How can I make NH understand that empty string means empty string?
You can write your Where clause like this:
.Where(Restrictions.On<ClassType>(obj => obj.Col1).IsNull ||
Restrictions.On<ClassType>(obj => obj.Col1).IsLike(#""))
Alternatively, if you're doing this on several queries you should consider creating a query extension:
public static class QueryExtention {
public static IQueryOver<E, F> WhereStringIsNullOrEmpty<E, F>(this IQueryOver<E, F> query, Expression<Func<E, object>> expression) {
var property = Projections.Property(expression);
var criteria = Restrictions.Or(Restrictions.IsNull(property),
Restrictions.Eq(property, string.Empty));
return query.Where(criteria);
}
}
Then you should be able to create something like:
.QueryOver<ClassType>()
.WhereStringIsNullOrEmpty(obj => obj.Col1)