Save part of Linq expression in Any and reuse - c#

I have following Linq queries:
var leaders = _db.Context.Person.Where(p => p.PersonGroup.Any(pg => pg.IsActive && !pg.IsDeleted && pg.GroupType == 'leader'))
var staff = _db.Context.Person.Where(p => p.PersonGroup.Any(pg => pg.IsActive && !pg.IsDeleted && pg.GroupType == 'staff'))
How do I save
pg => pg.IsActive && !pg.IsDeleted part to a variable so my query can be simplified to something like
var staff = _db.Context.Person.Where(p => p.PersonGroup.Any(pg => pg.IsActiveAndNotDeleted && pg.GroupType == 'staff'))
Thanks

So you have a sequence of Persons, where every Person has a property PersonGroup. Apparently PersonGroup is a sequence of zero or more similar items.
We don't know what these items are. What we do know, is that each of these items have Boolean properties IsActive and IsDeleted and a property GroupType which gives an indication of the type of the item: is it a leader, or a staff, or maybe something else.
Be aware: GroupType does not say anything about PersonGroup, but about one item in the PersonGroup. You didn't specify that all items in one PersonGroup have the same GroupType. As far as I know, it can be that PersonGroup has two items, one has GroupType leader and one has GroupType staff.
Requirement: Give me all Persons that have at least one item in property PersonGroup that is Active AND not IsDeleted AND has a third condition.
In your example, the third condition is pg.GroupType == leader, or pg.GroupType == staff. But it could be any condition on the type of items that are in PersonGroup.
I don't know the type of items that are in PersonGroup. Let's say they are items of class Item. Please replace this with the actual type of the items that are in PersonGroup.
My advice would be to create an extension method that takes as input an IQueryable<Person> and the third condition, and returns as output the query for all Persons that have at least one Item in property PersonGroup that is Active, not Deleted and that match the third condition.
If you are not familiar with extension methods, read Extension methods demystified
public static IQueryable<Person> WhereAnyActiveGroup(
this IQueryable<Person> persons,
Expression<Func<Item,Boolean>> thirdCondition)
{
return persons.Where(person => person.PersonGroup
.Where(item => item.IsActive && !item.IsDeleted)
.Where(thirdCondition)
.Any());
}
TODO: invent a proper method name.
In words: from the input sequence of Persons, keep only those persons that have at least one Item in property PersonGroup that is Active and not Deleted and that matches the thirdCondition.
Usage:
using (var dbContext = new PersonelContext())
{
var leaders = dbContext.Persons
.WhereAnyActiveGroup(person => person.GroupType == 'leader'))
.ToList();
var staff = dbContext.Persons
.WhereAnyActiveGroup(person => person.GroupType == 'staff'))
.ToList();
}
You can even concatenate this with other LINQ methods:
var result = dbContext.Persons.Where(person => person.City == "New York")
.WheraAnyActiveGroup(person => person.GroupType == 'staff')
.GroupBy(person => person.Name)
.ToList();

I think the simplest route would be some variation on providing a method that returns the result of a where:
private IEnumerable<Person> GetActiveInRole(string role){
return _db.Context.Person.Where(p => p.PersonGroup.Any(pg => pg.IsActive && !pg.IsDeleted && pg.GroupType == role));
}
And then use that and build on it:
var staff = GetActiveInRole("staff");
var s = staff.Where(p => p.Name == "John");

Related

How use Reflection to condition multiple properties to check for equality in a LINQ .Where statement, depending on what class is passed?

I'm trying to generalize a duplicate checker function, which depending on which type of object, checks the properties said class has (provided in a configuration) are equal to those in another list.
I have decided to create a Dictionary, which will accept a type string for the key (Book, Author, Shop, etc.) and an array of properties that need to be equal.
Example of Dictionary enties:
"Book", ["Title", "CoverImage", "NumberOfPages"]
"Author", ["Name", "Address", "SomethingElse"]
Then, I pass an object to the function and use Reflection to get the name of the type...
obj.GetType().Name;
... which I then use to fetch the right KVP from the Dictionary, meaning that if I pass a Book object, I get "Book". We then use that to get the configuration via ...
configDictionary["obj.GetType().Name"]
... which gives us the array of strings that are the properties that we need to check equality on.
I've gotten to the part where I need something along the lines of
list.Where(x => --> for each of the strings in the array - x.GetType.GetProperty(string) && --> same for next string && same for next string
... and then I need to top it off with an...
x.Id != obj.Id
To make sure we check for duplicates based on our logic (different id's and matches on all properties but has different Id's thus - a duplicate).
The end query should look like
Books:
someList.Where(x =>
x.Title == obj.Title
&& x.CoverImage == obj.CoverImage
&& x.NumberOfPages == obj.NumberOfPages
&& x.Id != obj.Id)
.FirstOrDefault();
Authors:
someList.Where(x => x.Name == obj.Name
&& x.Address == obj.Address
&& x.SomethingElse == obj.SomethingElse
&& x.Id != obj.Id)FirstOrDefault();
Try to avoid reflection because it can slow down your application. As an alternative you can create a dictionary and put all comparators into it:
var configDictionary = new Dictionary<string, List<Func<object, object, bool>>>
{
{
"Book",
new List<Func<object, object, bool>>
{
(b1, b2) => ((Book)b1).Title == ((Book)b2).Title,
(b1, b2) => ((Book)b1).CoverImage == ((Book)b2).CoverImage,
(b1, b2) => ((Book)b1).NumberOfPages == ((Book)b2).NumberOfPages,
(b1, b2) => ((Book)b1).Id != ((Book)b2).Id,
}
},
// same for Authors
};
Now you can use it in Where method:
var typeName = obj.GetType().Name; // here we using Reflection but once per collection, not per each item
var first = someList.Where(x => configDictionary[typeName].All(f => f(x, obj))).FirstOrDefault();
Also, because FirstOrDefault also has overload that accept predicate last line can be rewritten to:
var first = someList.FirstOrDefault(x => configDictionary[typeName].All(f => f(x, obj)));
A better solution will be creating custom attribute which will tag property. Then in class override default method Equals which will get all properties with this attribute and return equality.

Remove List elements that are same

I currently have two lists of Directory Info. Candidatelist & VersionList. VersionList being a sublist of candidate list. I'm trying to remove all the elements from candidate list that appear in version list. So if canadidate list has 177 elements and version list has 77 then we have 100 elements left in candidate list. To be more explicit of whats inside the list. Each element correspons to a directory folder name who has a name and a parent folder name. It is possible that the directory name has duplicates but diffeerent parents. I tried doing this but I'm not necesarily getting the correct result Take a look:
candidateList.RemoveAll(x => versionslist.Any(y => y.Name == x.Name) && versionslist.Any(y => y.Parent.Name == x.Parent.Name));
return candidateList;
Your current query is not constraining business requirements to a single version list item. It's making the two queries separately. It's saying:
Are there any versionList items where the Name matches the current candidateList item name
If yes:
Are there any versionList items where the Parent.Name matches the current candidateList item Parent.Name
If yes, remove the item from the candidate list. Instead, you should be querying for a versionList item that meets both requirements pieces at the same time.
candidateList.RemoveAll(x => versionslist.Any(y => y.Name == x.Name && y.Parent.Name == x.Parent.Name));
return candidateList;
This is now saying:
Are there any versionList items where the Name and Parent.Name matches the current candidateList item Name and Parent.Name, respectively.
You can use IEnumerable.Except() method with custom IEqualityComparer:
var differences = candidateList.Except(versionList, new DirectoryInfoComparer());
The EqualityComparer could look like this:
public class DirectoryInfoComparer : IEqualityComparer<DirectoryInfo>
{
bool IEqualityComparer<DirectoryInfo>.Equals(DirectoryInfo x, DirectoryInfo y)
{
return (x.Name == y.Name) && (x.Parent.Name == y.Parent.Name);
}
int IEqualityComparer<DirectoryInfo>.GetHashCode(DirectoryInfo obj)
{
if (Object.ReferenceEquals(obj, null))
return 0;
return obj.GetHashCode();
}
}
As you mentioned, you want to remove all the elements from version list that appear in canadidate list. I think then your syntax should be like:
VersionList.RemoveAll(x => candidateList.Any(y => y.Name == x.Name && y.Parent.Name == x.Parent.Name);
return VersionList;
if it is vice versa, then use
candidateList.RemoveAll(x => VersionList.Any(y => y.Name == x.Name && y.Parent.Name == x.Parent.Name);
return candidateList;

Combining search criteria

I have an MVC project with two DropDown-boxes where you can choose from two different lists. In the first list you choose an age and in the second list you chose a name.
Separately, they both work fine. If I choose a name, the view returns all the people with the given name and if I choose age, the same thing happens. The problem is when I try to combine the two lists (dropdowns). Here is the code:
else if (!string.IsNullOrEmpty(age) && (string.IsNullOrEmpty(name)))
{
return View(person.Where(d => d.age == ages));
}
else if (string.IsNullOrEmpty(age) && (!string.IsNullOrEmpty(name)))
{
return View(person.Where(c => c.name == names));
}
else if (!string.IsNullOrEmpty(age) && (!string.IsNullOrEmpty(name)))
{
//CODE TO RETURN VIEW MATCHING BOTH CRITERIA
// For example: IF age is 25 and Name is Bob, i´d like to display all
// 25-year olds named BOB...If there is no 25y/old named bob
// return an empty list
}
You can combine .Where() clauses and simplify all of this. Then just build the criteria and have a single return statement. Something like this:
if (!string.IsNullOrEmpty(age))
person = person.Where(d => d.age == ages);
if (!string.IsNullOrEmpty(name))
person = person.Where(c => c.name == names);
return View(person);
That way any supplied criteria is applied.
You can combine conditions with linq where method
if (!string.IsNullOrEmpty(age) && !string.IsNullOrEmpty(name))
{
return View(person.Where(p => p.name == name && p.age == age).ToList());
}
This code returns persons with name and age corresponding to criteria or an empty list if no result found

Linq lambda entities , does not contain definition

I have this query code here :
//Get all records based on ActivityID and TaskID.
public IList<Model.questionhint> GetRecords1(int listTask, int listActivity)
{
IList<Model.questionhint> lstRecords = context.questionhints.ToList();
return lstRecords.GroupBy(x => new { x.QuestionNo, x.ActivityID, x.TaskID }).Where(a => a.TaskID == listTask && a.ActivityID == listActivity).ToList();
}
The error lies in the .Where statement, it says does not contain definition for ActivityID and TaskID.
Full error :
'System.Linq.IGrouping' does not contain a definition for 'ActivityID' and no extension method 'ActivityID' accepting a first argument of type 'System.Linq.IGrouping' could be found (are you missing a using directive or an assembly reference?)
I am weak in query statements, basically I want to retrieve records from database where activity id = something and task id = something and group them by questionNo, activityId and Task ID .
The simplest fix here is: filter (where) before you group; this will also reduce the work that the grouping has to do:
return context.questionhints
.Where(a => a.TaskID == listTask && a.ActivityID == listActivity)
.GroupBy(x => new { x.QuestionNo, x.ActivityID, x.TaskID })
.ToList();
The reason it isn't working in your original code is, as already mentioned, that GroupBy returns a sequence of groups - each of which has a .Key (your anonymous type) and is itself an IEnumerable<T> sequence of the items in that group.
However! Your method claims to return IList<Model.questionhint>; your grouped data is not, and will never be, an IList<Model.questionhint> - it will be an IList<IGrouping<{some anonymous type, Model.questionhint>>. So: you cannot group like that if you are claiming that it is an IList<Model.questionhint> - and since the grouping is an anonymous type, you can't change the return type to match. You have two choices:
don't group
group by something declarable (a custom type, or a Tuple<...>), and change the return type to match
For example:
public IList<IGrouping<Tuple<int,int,int>,Model.questionhint>>
GetRecords1(int listTask, int listActivity)
{
return context.questionhints
.Where(a => a.TaskID == listTask && a.ActivityID == listActivity)
.GroupBy(x => Tuple.Create(x.QuestionNo, x.ActivityID, x.TaskID))
.ToList();
}
change it to Where(a => a.Key.TaskID == listTask && a.Key.ActivityID == listActivity)
You are dealing with an IGrouping, of which the Key property is the anonymous object new { x.QuestionNo, x.ActivityID, x.TaskID }
This solves the original error, but now we are trying to return IGroupings, which is not the correct return type. A cleaner way of doing it would be
var groups = lstRecords.GroupBy(x => new { x.QuestionNo, x.ActivityID, x.TaskID }).Where(a => a.Key.TaskID == listTask && a.Key.ActivityID == listActivity);
IList<Model.questionhint> questionHints = new List<Model.questionhint>();
foreach(var group in groups)
{
questionHints.AddRange(group);
}
return questionHints;
Note, this code is untested. You can do all this in one linq line (as im sure Mark will), however i tend to split it for readability
Alternative
If your goal is to get all the questionHints that match the criteria, what is wrong with a simple Where clause?
lstRecords.Where(a=>a.TaskID == listTask && a.ActivityID == listActivity).ToList();
References
http://msdn.microsoft.com/en-us/library/bb344977.aspx

LINQ return items in a List that matches any Names (string) in another list

I have 2 lists. 1 is a collection of products. And the other is a collection of products in a shop.
I need to be able to return all shopProducts if the names match any Names in the products.
I have this but it doesn't seem to work. Any ideas?
var products = shopProducts.Where(p => p.Name.Any(listOfProducts.
Select(l => l.Name).ToList())).ToList();
I need to say give me all the shopproducts where name exists in the other list.
var products = shopProducts.Where(p => listOfProducts.Any(l => p.Name == l.Name))
.ToList();
For LINQ-to-Objects, if listOfProducts contains many items then you might get better performance if you create a HashSet<T> containing all the required names and then use that in your query. HashSet<T> has O(1) lookup performance compared to O(n) for an arbitrary IEnumerable<T>.
var names = new HashSet<string>(listOfProducts.Select(p => p.Name));
var products = shopProducts.Where(p => names.Contains(p.Name))
.ToList();
For LINQ-to-SQL, I would expect (hope?) that the provider could optimise the generated SQL automatically without needing any manual tweaking of the query.
You could use a join, for example:
var q = from sp in shopProducts
join p in listOfProducts on sp.Name equals p.Name
select sp;
A fuller guide on join is here.
You could create an IEqualityComparer<T> that says products with equal names are equal.
class ProductNameEqulity : IEqualityComparer<Product>
{
public bool Equals(Product p1, Product p2)
{
return p1.Name == p2.Name
}
public int GetHashCode(Product product)
{
return product.Name.GetHashCode();
}
}
Then you can use this in the Intersect extension method.
var products = shopProducts.Intersect(listOfProducts, new ProductNameEquality());
Try this please
var products = shopProducts.Where(m=> listOfProducts.Select(l=>l.Name).ToList().Contains(m=>m.Name));
var products = shopProducts
.Where(shopProduct =>
listOfProducts.Any(p => shopProduct.Name == p.Name))
.ToList();

Categories