LINQ: Generate "AND" Expression instead of "OR" when using "CONTAINS" - c#

I have this List:
string[] countries = {
"USA",
"CANADA"
};
When I run this query :
query = (from user in db where
user.Orders.Any(order => order.Price > 10 &&
countries.Contains(order.DestinationCountry)))
Output is a list of users that have Orders sent to "USA" OR "Canada".
but I want the list of users that have Orders sent to both "USA" AND" "CANADA".
I can do this using below code but i'm searching for a pure linq solution without any ForEach:
foreach (country in countries) {
query = (from user in query where
user.Orders.Any(order => order.Price > 10 &&
order.DestinationCountry == country));
}
Answers:
A. Using .Aggregate()
Generated query is just like For Each.
B.where countries.All(c => user.Orders.Any(o => o.Price > 10 && o.DestinationCountry == c))
When there is no element in Countries List (When I want all users based only on Price parameter), the result is not correct and other parameter is not considered!
Update 1:
I have tried .All() instead of .Contains() before posting and it returns 0 users.
Update 2:
I have updated my question to make it closer to the real problem.
lets say Country is not the only parameter.
Update 3:
Checked some answers and added the result to my question.

So you want a list of the users such that all the countries in the list are present in the set of order destinations?
Logically, that would be:
query = from user in db
where countries.All(c => user.Orders.Any(o => o.DestinationCountry == c))
select ...;
However, I'm not confident that EF will do what you want with that. It's not clear to me what the right SQL query would be to start with - in a simple way, at least.

query =
db.Users.Where(user =>
countries.All(country =>
user.Orders.Any(order =>
order.DestinationCountry == country)))

You can do it like this:
query = (from user in db where
user.Orders
.Where(o => countries.Contains(o.DestinationCountry))
.GroupBy(o => o.DestinationCountry)
.Count() == countries.Count
);
The idea is to keep only the orders going to countries of interest, then group by country, and check that the number of groups equals the number of countries.

It's possible using Enumerable.Aggregate:
query = countries.Aggregate(query,
(q, c) =>
from user in q
where user.Orders.Any(order => order.DestinationCountry == c)
select user);
but really, this is harder to understand than your foreach loop, so I'd just go with that.
Note that although I refer to a member of Enumerable, that member of Enumerable is actually building up an IQueryable<User> query chain just like your foreach loop, so this will not cause the filtering to move to the client.

Related

Linq nested loop search one specific top level element , collection within collections

I want to get the company whose employees id card issued with the specific number, sort of finding the exact element inside nested collection.
Using first or default 3 times does not seems to be a correct way.
> var company = cprIdentificationReply.Companies
> .FirstOrDefault(x => (x.Employee
> .FirstOrDefault(y => (y.IDCardIssued
> .FirstOrDefault(z => z.CardNumber
> .Equals(number,StringComparison.InvariantCultureIgnoreCase))) != null)
> != null));
What can be a proper way of achieving the same?
You may want to use the Any extension method:
var companies = cprIdentificationReply.Companies
.Where(x => (x.Employee
.Any(y => (y.IDCardIssued
.Any(z => z.CardNumber
.Equals(number, StringComparison.InvariantCultureIgnoreCase)
)
)
).ToList();
If you want better looking code why don't use LINQ query syntax.
I always find it easier to read LINQ query when looking at someone else's code, especially for complex operations.
Something like this:
var company = (from company in cprIdentificationReply.Companies
from empl in company.Employee
from idCardIss in empl .IDCardIssued
where idCardIss.CardNumber.Equals(number, StringComparison.InvariantCultureIgnoreCase)
select c).FirstOrDefault();

How to convert a lambda expression to LINQ?

How can I convert this expression to LINQ?
var result = users.FirstOrDefault(x => x.Name == userName)?
.Groups.FirstOrDefault(x => x.Group == userGroup);
I've started with:
var result = (from u in users
where u.Name == userName
select u).FirstOrDefault()?
My class is:
public class User
{
public string Name { get; set; }
public IEnumerable<Group> Groups { get; set; }
}
When creating this query, I don't have a separate groups list with which I can make a join on 2 tables.
But that's how far I managed to go. Is it possible to do a join within the same query?
I guess you want this:
var result = (from g in ((from u in users
where u.Name == userName
select u).FirstOrDefault().Groups)
where g == userGroup
select g).FirstOrDefault();
What you mean is how to convert method (or fluent) syntax to query (or comprehension) syntax. The first thing to note though is there's not one LINQ expression. The statement consists of two LINQ statements...
var user = users.FirstOrDefault(x => x.Name == userName);
var result = user?.Groups.FirstOrDefault(x => x.Group == userGroup);
...both of which can be written in query syntax, of which your starting point would be the first one.
However, the statement can be rewritten as one LINQ statement using SelectMany:
var result = users.Where(x => x.Name == userName)
.SelectMany(u => u.Groups.Where(g => g.Group == userGroup))
.FirstOrDefault();
This statement can be rewritten in one query-syntax statement:
var result = (from u in users
where u.Name == userName
from g in u.Groups
where g.Group == userGroup
select g).FirstOrDefault();
The advantage is that you don't need the null-propagation operator, which, by the way, you didn't apply sufficiently in your own statement.
One possible issue is that the results aren't necessarily identical. Originally you query a first user meeting a condition and of its groups a first group meeting another condition. The alternative query queries all users meeting a condition and from their groups the first one that meets another condition. So the first query may not return a result where the second does (if the matching group is not from the first user).
This may be an improvement or a flaw, I don't know. If the first condition uniquely identifies users it doesn't matter; the results will be the same. If it doesn't you may have to question its value because in a way it will return you a "random" user. You may want to use a lambda expression that narrows down the search to one specific user.

c# IEnumerable<Object> LINQ GroupBy - Merge 2 Groups

I'm working on some LINQ GroupBy logic and I can't think how to elegantly and efficiently get this to work.
Basically, I have an IEnumerable<Thing> object (which is in the correct order!), where each Thing object has a RootId property. I want to group these objects on their RootId, which I have working:
IEnumerable<Thing> things; // Already has value assigned
var groups =
(from thing in things
group thing by thing.RootId into thingGroup
select thingGroup.ToArray())
.ToList();
groups is of type List<Thing[]>
Now here is the problem!
The above example is returning 5 items in the list. But, how would I merge 2 of the arrays into 1, leaving 4 items (again, keeping the order of course)??
The reason why is because 2 of the items has different RootId's but I want them to be treated the same i.e. grouped together.
I was going to concat and manipulate the arrays after the LINQ statement, but really it needs to be done as part of the group by/LINQ - any ideas?
Let me know if further examples or information is needed.
Thanks!
The merging criteria will be a manual process, so I was thinking of passing it into the groupby method like so:
var rootIdsToMerge = new List<Tuple<ID, ID>>
{
new Tuple<ID, ID>(rootIdOne, rootIdTwo),
new Tuple<ID, ID>(rootIdThree, rootIdFour)
};
So any group item with a RootId of rootIdOne will be merged with the group item with a RootId of rootIdTwo, and so on.
Since you are not using the grouping Key, you can associate the Item2 from the mapping to Item1 as a RootId key to group by:
var groups =
(from thing in things
group thing by rootIdsToMerge.FirstOrDefault(e => e.Item2 == thing.RootId)?.Item1 ?? thing.RootId
into thingGroup
select thingGroup.ToArray())
.ToList();
Or in pre C#6 (no .? operator):
var groups =
(from thing in things
let mergeWith = rootIdsToMerge.FirstOrDefault(e => e.Item2 == thing.RootId)
group thing by mergeWith != null ? mergeWith.Item1 : thing.RootId
into thingGroup
select thingGroup.ToArray())
.ToList();
Update: If you just want to consolidate a list of RootIds, then you can use a combination of Contains and First:
List<ID> rootIdsToMerge = ...;
var groups =
(from thing in things
group thing by rootIdsToMerge.Contains(thing.RootId) ? rootIdsToMerge.First() : thing.RootId
into thingGroup
select thingGroup.ToArray())
.ToList();
The variants with
List<List<ID>> rootIdsToMerge = ...;
are similar to the initial variant with tuples:
var groups =
(from thing in things
group thing by rootIdsToMerge.FirstOrDefault(ids => ids.Contains(thing.RootId))?.First() ?? thing.RootId
into thingGroup
select thingGroup.ToArray())
.ToList();
or
var groups =
(from thing in things
let mergeList = rootIdsToMerge.FirstOrDefault(ids => ids.Contains(thing.RootId))
group thing by mergeList != null ? mergeList.First() : thing.RootId
into thingGroup
select thingGroup.ToArray())
.ToList();

how to take 100 records from linq query based on a condition

I have a query, which will give the result set . based on a condition I want to take the 100 records. that means . I have a variable x, if the value of x is 100 then I have to do .take(100) else I need to get the complete records.
var abc=(from st in Context.STopics
where st.IsActive==true && st.StudentID == 123
select new result()
{
name = st.name }).ToList().Take(100);
Because LINQ returns an IQueryable which has deferred execution, you can create your query, then restrict it to the first 100 records if your condition is true and then get the results. That way, if your condition is false, you will get all results.
var abc = (from st in Context.STopics
where st.IsActive && st.StudentID == 123
select new result
{
name = st.name
});
if (x == 100)
abc = abc.Take(100);
abc = abc.ToList();
Note that it is important to do the Take before the ToList, otherwise, it would retrieve all the records, and then only keep the first 100 - it is much more efficient to get only the records you need, especially if it is a query on a database table that could contain hundreds of thousands of rows.
One of the most important concept in SQL TOP command is order by. You should not use TOP without order by because it may return different results at different situations.
The same concept is applicable to linq too.
var results = Context.STopics.Where(st => st.IsActive && st.StudentID == 123)
.Select(st => new result(){name = st.name})
.OrderBy(r => r.name)
.Take(100).ToList();
Take and Skip operations are well defined only against ordered sets. More info
Although the other users are correct in giving you the results you want...
This is NOT how you should be using Entity Framework.
This is the better way to use EF.
var query = from student in Context.Students
where student.Id == 123
from topic in student.Topics
order by topic.Name
select topic;
Notice how the structure more closely follows the logic of the business requirements.
You can almost read the code in English.

Linq Remove results with ALL statement

Hi I'm trying to use Linq to remove "all" entities from a list.
Problem: I'm searching for users that have certain certificates in my database. Thing is that it returns them row by row.... But what I need to check is: If the user holds all the required certificates. This should be checked against my int array.
This is my array: [3,5,16], now I want to delete all user who does not have all three of those from the list. Name of the array in code is mandatory!
The listitems I get back looks like this
listitem.CertificateValue
listitem.Uid
listitem.NameOfPerson
So basicly for this example Peter has three rows in the list, in this case all the rows needed to stay in the list. But Philip only has 2 rows and hence both of these should be deleted since he does not fullfill the total search criteria.
Also copyOfMandatoryis just to not mess with the original collection and cause an expection(collection size changed).
foreach (var item in copyOfMandatory)
{
if (!mandatoryusers.All(i => mandatory.Contains(i.CertificateValue)
|| i.Uid == item.Uid))
{
mandatoryusers.RemoveAll(i => i.Uid == item.Uid);
}
}
UPDATE
RemoveAll works like a charm it the if statement that does not work as expected.
Doing this it does not take away any part of the list, I began wiht && instead of || but whne doing that it kills everything but the last person it encounters as long as he/she fullfills the search criteria.
Anyone have a hint on how to do this?
I would try something like that
var uIdToRemove = mandatoryusers.GroupBy(m => m.Uid)
.Where(g => mandatory.Except(g.Select(s => s.CertificateValue)).Any())
.Select(g => g.Key).ToList();
mandatoryusers.RemoveAll(x => uidToRemove.Contains(x.Uid));
Your All call is not granular enough: it is trying to ensure that ALL entries exist at all times... Not that all entries PER USER exist.
Try converting each entry to a dictionary:
var dict = new Dictionary<int, List<ItemType>>();
foreach (var mandatoryItem in mandatoryItems)
{
List<ItemType> itemTypeValue = null;
if (!dict.TryGetValue(mandatoryItem.Uid, out itemTypeValue)
{
itemTypeValue = new List<ItemType>();
dict.Add(mandatoryItem.Uid, itemTypeValue);
}
itemTypeValue.Add(mandatoryItem);
}
Now you have all ItemType at the key of Uid. From here, use LINQ:
mandatoryusers = mandatoryusers.Where(i => dict[i.Uid].All(x => mandatory.Contains(x.CertificateValue));
Your if All criteria is off.
if (!mandatoryusers.All(i => mandatory.Contains(i.CertificateValue)
|| i.Uid == item.Uid))
{
mandatoryusers.RemoveAll(i => i.Uid == item.Uid);
}
It needs to be with an && not an || and you should call Any() instead of All()
if (!mandatoryusers.Any(i => mandatory.Contains(i.CertificateValue)
&& i.Uid == item.Uid))
{
mandatoryusers.RemoveAll(i => i.Uid == item.Uid);
}
Hopefully I understood what your logic and question correctly.
Your if statement isn't correct (as you stated) - it's attempting to check whether all items contain a certificate with an id in mandatory or where the userid is the current item. What you should be doing is filtering by userid first and then checking the certificates.
This isn't the way I would do it, though. I'd group the results by User and then check the certificates
var usersWithAllCertificates = mandatoryUsers.GroupBy(mu => mu.Uid)
//Select the ones that have all 3 certificates
.Where(g => g.Select(u => u.CertificateValue)
.Intersect(mandatory).Count() == 3)
.Select(g => g.ToList());
The Intersect operator will combine the lists and the result will be the items that are the same in both lists. So, if the user has all 3 certificates (3, 5 and 16) the result of the intersect will be 3 items. The usersWithAllCertificates object will include all the users you want. This is explicitely selecting the values you want instead of removing the ones you don't want, which imo is a better way of going about it. Note that this assumes each user is only in the list once (i.e. only has 3 certificates)

Categories