Remove child list item when checking grandchild list using Linq - c#

The following code shows how I am assigning data into IEnumerable<UnCompletedJobDetailsBO>.
There is a list (IEnumerable<UnCompletedJobDetailsBO>) that has another list (List<JobDetailsBO>), with that child list (List<JobDetailsBO>) having a list on it. But the AllocationDetailList only ever has one list item.
public IEnumerable<UnCompletedJobDetailsBO> GetControlDetails(DateTime startDate)
{
var controlDetails =
(from booking in db.BookingDetail
where booking.BookingDateTime >= startDate
orderby booking.DocketNo
select new UnCompletedJobDetailsBO()
{
CustomerName = booking.Customer.Name,
CompanyName = booking.CompanyDetail.Name,
JobList =
(from job in db.BookingJob.Where(x => x.BookingID == booking.BookingID) //get job list
select new JobDetailsBO()
{
JobID = job.JobID,
JobType = job.JobType,
ItemName = job.ItemName,
AllocationDetailList =
(from jobAllocationDetail in db.JobAllocation
join returnUnCollected in db.JobReturnUnCollected
on jobAllocationDetail.JobAllocationDetailID
equals returnUnCollected.JobAllocationDetailID
into returnJob
from returnUnCollected in returnJob.DefaultIfEmpty()
where (jobAllocationDetail.Booking.BookingID == booking.BookingID)
select new AllocationBO()
{
JobUnCollectedID = returnJob.JobUnCollectedID,
JobType = jobAllocationDetail.JobType,
CurrentStatus = jobAllocationDetail.CurrentStatus,
}).DefaultIfEmpty().ToList(),
}).DefaultIfEmpty().ToList(),
}).ToList();
return controlDetails;
}
I want to remove the JobList item if the inner list (AllocationDetailList) item satisfies the condition below. Sometimes AllocationDetailList may be null, so I check that also. But when I write below query, it does not remove that particular JobList item that satisfies the condition. Thanks in advance.
public List<UnCompletedJobDetailsBO> RemovePODFromSelectedList(
List<UnCompletedJobDetailsBO> unCompletedJobDetailsBO)
{
unCompletedJobDetailsBO
.SelectMany(y => y.JobList)
.ToList()
.RemoveAll(x => ((x.AllocationDetailList[0] != null) ?
x.AllocationDetailList[0].JobType == "D" &&
x.AllocationDetailList[0].JobUnCollectedID == null &&
x.AllocationDetailList[0].CurrentStatus == 5 :
x.AllocationDetailList.Count > 1));
return unCompletedJobDetailsBO;
}

Without a good, minimal, complete code example, I'm not sure that any performance concern can be addressed. It's hard enough to fully understand the question as it is, but without being able to actually test the code, to reproduce and observe a specific performance concern, it's hard to know for sure where your concern specifically lies, never mind how to fix it.
That said, from the code you posted, it is clear why items are not being removed from the list. The basic issue is that while the SelectMany() method does have the effect of allowing you to enumerate all of the elements from all of the different JobList objects as a single enumeration, the elements are enumerated as a new enumeration.
When you call ToList(), you are creating a whole new list from that new enumeration, and when you call RemoveAll(), you are only removing elements from that new list, not the lists from which they originally came.
You say you can get it to work with a for loop. I assume you mean something like this:
public List<UnCompletedJobDetailsBO> RemovePODFromSelectedList(
List<UnCompletedJobDetailsBO> unCompletedJobDetailsBO)
{
foreach (var item in unCompletedJobDetailsBO)
{
item.JobList.RemoveAll(x => ((x.AllocationDetailList[0] != null) ?
x.AllocationDetailList[0].JobType == "D" &&
x.AllocationDetailList[0].JobUnCollectedID == null &&
x.AllocationDetailList[0].CurrentStatus == 5 :
x.AllocationDetailList.Count > 1));
}
return unCompletedJobDetailsBO;
}
Note: there is no need to return unCompletedJobDetailsBO. That entire object is unchanged, never mind the variable. The only thing the code is modifying is each individual JobList object within the passed-in object's members. I.e. the above method could actually have a return type of void, and the return statement could be removed entirely.
It is possible you could speed the code up by removing the elements in a different way. The List<T>.RemoveAll() method is in fact reasonably efficient, with O(n) cost. But it still involves copying all of the data in the list after the first element that is removed (so that all the elements are shifted down in the list). If you have to have the list ordered, this may be as good as you can do, but if not, you could process the removal differently, or use a different data structure altogether, something unordered where removal of one or more elements costs less.
But again, without more details and without a good example to work with addressing that particular issue doesn't seem practical here.

The condition
x.AllocationDetailList[0] != null
will throw exception if there is no item in the AllocationDetailList. Instead you need to check
x.AllocationDetailList!=null && x.AllocationDetailList.Count>0.
Also .ToList() after SelectMany in your code will create a new list and items will be removed from that new list instead of unCompletedJobDetailsBO. You need to modify the remove function as below
unCompletedJobDetailsBO.ForEach(y => y.JobList.RemoveAll(x => ((x.AllocationDetailList != null && x.AllocationDetailList.Count>0)
?
x.AllocationDetailList[0].JobType == "D"
&& x.AllocationDetailList[0].JobUnCollectedID == null
&& x.AllocationDetailList[0].CurrentStatus == "5"
:
x.AllocationDetailList.Count > 1
)
));

Related

Trying to Filter a collection using LINQ where the collection has a collection which also has a collection with a property that is nullable

Ok, this has been a pretty interesting problem for me to solve, so I will try and do my best to ask this in a way in which you can understand.
So, I have a collection of objects which contains a collection of objects which contains a collection of objects which has a property that sometimes can be null.
It looks something like this pseudocode:
Class ObjectA
{
public IEnumerable<ObjectB>
}
Class ObjectB
{
public IEnumerable<ObjectC>
}
Class ObjectC
{
property? a;
}
Ok, now I basically need to filter out all of the ObjectC where the property = someValue or the property is null.
I tried this: (remember, this is just an example, not real code)
IEnumerable<ClassA> collection;
List<string> filters; // This contains a list of filters
collection = collection.Where(a => a.collectionB.All(b =>
b.collectionC.Where(c => !filter
.Contains(c.Property))?
.Count() == 0))
.ToList();
So, the problem is that if I match a filter to the c.Property nothing happens. It should remove that one from the collection but it isn't. I also need to not filter out any of the ones where the c.Property == null or the collectionC == null.
EDIT: what I am really trying to accomplish is if some c.Property = 'x' and some c.Property = 'y' and some c.Property = null, I want to remove from my collection anything where c.Property = 'x' but leave the rest.
collection.Where(a=>a.collectionB.Any(b=> b.collectionC == null ||
b.collectionC.Any(c=>c.Property == null || filters.Contains(c.Property))))

LINQ optimization using single query on collection of integers

I have a list of integers as below
List<int> myCollection = new List<int> { 2625 };
I am checking below condition
if(myCollection.Count() == 1 && myCollection.Any(number=> number == 2625))
{
// Do something
}
How can I optimize my query so that I can include both conditions single query?
Note: MyCollection may contain multiple elements hence I have used Any().
One obvious optimization would be to use List instance properties:
if(myCollection.Count == 1 && myCollection[0] == 2625))
{
// Do something
}
Actually, here you have one query. Not two. Since your collection is a list, Count() will be resolved to Count property of list. That being said, nothing actually will happen here
myCollection.Count() == 1
except from getting the value of Count.
The only query happens here
myCollection.Any(number=> number == 2625)
Furthermore, since the first condition you check if the Count is 1, if it doesn't, then Any wouldn't be evaluated at all. (This happens because we make use of &&).
I'm not sure why you want to
Optimize this, but if there is a single element why use .Any().
You could simply do
if(myCollection.Count() == 1 && myCollection.Single() == 2625)
{
// Do something
}

Convert nested for each loop to LINQ

I have a for each loop to get data which is very time consuming.any suggestion to convert this to linq. Thanks in advance.
iListReport = obj.GetClosedReports();
string sRepType ="";
foreach (ReportStatisticsInfo item in reportStatistic)
{
sRepType = item.ReportName.Trim();
IList<string> lastClosedReport = new List<string>();
foreach (TaskListInfo taskInfo in iListReport)
{
string reportName = taskInfo.DocumentName.Trim();
if (string.Compare(sRepType, reportName, true) == 0)
{
if (taskInfo.ActionID == Convert.ToInt16(ReportAction.Close) && !lastClosedReport.Contains(taskInfo.DocumentID))
{
iClosedreportCount += 1;
lastClosedReport.Add(taskInfo.DocumentID);
}
}
}
}
Here you go. I've done a pretty literal translation of your code into LINQ which will hopefully help you to see how I've converted it.
Note the use of the let keyword which allows you to declare a range variable (which allows you to perform your trim once and then use the result in multiple places).
Also note the use of group by at the bottom of the LINQ query to ensure we only take the first occurence of each documentID.
IList iListReport = obj.GetClosedReports();
var query = from item in reportStatistic
let sRepType = item.ReportName.Trim()
from taskInfo in iListReport
let reportName = taskInfo.DocumentName.Trim()
where string.Compare(sRepType, reportName, true) == 0
&& taskInfo.ActionID == Convert.ToInt16(ReportAction.Close)
//here's how we make sure we don't get the same documentID twice
//we group by the id and then take the first
group taskInfo by taskInfo.DocumentID into grouping
select grouping.First().DocumentID;
var lastClosedReport = query.ToList();
iClosedreportCount = lastClosedReport.Count;
How to convert a foreach loop to LINQ
Here are some comparisons of your code against LINQ version to help you out if you've got to do a conversion again sometime. Hopefully this will help anyone else out there that has got to convert a foreach loop to LINQ.
1. foreach and from
You can perform a straight swap of the foreach clause for a LINQ from clause. You can see that this:
foreach (ReportStatisticsInfo item in reportStatistic)
has become this:
from item in reportStatistic
2) Variable declaration and the let keyword
When you declare variables within your foreach, you can swap them out for the LINQ let statement. You can see that this declaration:
sRepType = item.ReportName.Trim();
has become:
let sRepType = item.ReportName.Trim()
3) if statements and the where clause
Your if statements can go inside the where clause. You can see that the following two if statements:
if (string.Compare(sRepType, reportName, true) == 0)
if (taskInfo.ActionID == Convert.ToInt16(ReportAction.Close)
have become this where clause
where string.Compare(sRepType, reportName, true) == 0
&& taskInfo.ActionID == Convert.ToInt16(ReportAction.Close)
4) Using group by to remove duplicates.
It's all been quite simple so far because everything has just been a straight swap. The most tricky part is the bit of code where you prevent duplicates from appearing in your result list.
if (taskInfo.ActionID == Convert.ToInt16(ReportAction.Close)
&& !lastClosedReport.Contains(taskInfo.DocumentID))
{
iClosedreportCount += 1;
lastClosedReport.Add(taskInfo.DocumentID);
}
This is tricky because it's the only part that we have to do a bit differently in LINQ.
Firstly we group the 'taskInfo' by the 'DocumentID'.
group taskInfo by taskInfo.DocumentID into grouping
Then we take the first taskInfo from each grouping and get it's ID.
select grouping.First().DocumentID;
A note about Distinct
A lot of people try to use Distinct to get rid of duplicates. This is fine when we're using primitive types, but this can fail when you're using a collection of objects. When you're working with objects Distinct will do a reference comparison of the two objects. This will fail to match objects that are different instances but happen to have the same ID.
If you need to remove duplicates based upon a specific property within an object, then the best approach is to use a group by.
With LINQ you'll get a single IEnumerable<string> with duplicates
from item in reportStatistic
from taskInfo in iiListReport
where (string.Compare(item.ReportName.Trim(), taskInfo.DocumentName.Trim(), true) == 0)
&& taskInfo.ActionID == Convert.ToInt16(ReportAction.Close)
select taskInfo.DocumentID
You can then Distinct().GroupBy(d => d.taskInfo)

Displaying specific elements of a collection's subcollection

I have a List collection that contains a List subcollection as a property within it, and I want to filter out items in that subcollection based on the value of certain properties.
To simplify, I'll call the main collection THING and the subcollection SUBTHING. They are different types. THINGS can have 1 to many SUBTHINGS. SUBTHING has 2 properties I want to filter by, PROP1 should equal 1 (it can equal 1,2,3) and PROP2 should not be NULL (it can contain a string).
So when I use a query like the one below it seems to give me what I want (though I'm not sure All() is doing what I expect):
search = from c in search
where c.SUBTHING.All(s=>s.PROP1==1)
select c;
Then I get suspicious when I add the other property:
search = from c in search
where c.SUBTHING.All(s=>s.PROP1==1 && s.PROP2 != NULL)
select c;
And I get THINGS that have PROP2 as Null.
When I switch to Any() I lose all filtering on SUBTHING and it shows SUBTHINGS where PROP1 = 1,2,3 and where PROP2 is NULL and not NULL.
What I'm trying to get is a collection that lists all THING IDs and then lists the Name of all SUBTHINGS, sort of like this:
THING.ID
SUBTHING.Name
SUBTHING.Name
THING.ID
SUBTHING.Name
SUBTHING.Name
Is this possible to also filter SUBTHINGS while filtering THINGS with LINQ since THING and SUBTHING are two different types?
Try something like this:
search =
from c in search
where c.SUBTHING.All(s=>s.PROP1==1 && s.PROP2 != NULL)
select new {
ThingId = c.ThingID,
Something = c.SomeThing.Select(x=>x.Name)
};
To apply filter on subitems try:
from product in products
where product.productid == 1
from image in product.productimages
where image.ismainimage
select image.imagename
From : 101 linq queries
One way is using Enumerable.Where and an anonymous type:
var result = from thing in search
from subthing in thing.subthings
where subthing.prop1 == 1 && subthing.prop2 != null
select new {ID = thing.ID, Name = subthing.Name};
foreach(var x in result)
{
Console.WriteLine("ID={0} Name{1}", x.ID, x.Name);
}
You need a projection as you are querying over the parent entity (THING) but in the result set you want to only have a subset of its SUBTHINGS.
You can do it e.g. in the following way:
class Thing
{
Thing(Thing original, IEnumerable<Subthing> subthings)
{
// Initialize based on original and set the collection
//
...
}
}
and then run the query like this:
var filtered = from c in search
select new Thing(c, c.Subthings.Where(x => x.PROP1 == 1 && x.PROP2 != null))
I'm not sure any of these answers really give you what you want (although they're close). From my understanding, you want a list of THINGs in which at least 1 SUBTHING has the values you're interested in (in this case, Prop1 == 1 and Prop2 != null). There are a few options here, just depends on whether you're working from a THING or a SUBTHING perspective.
Option 1: THING approach.
You're looking at any THING that has a SUBTHING with your condition. So:
var result = from thing in search
where thing.Subthings.Any(tr => tr.Prop1 == 1 && tr.Prop2 != null)
select new { ID = thing.ID, Names = thing.Subthings.Where(tr => tr.Prop1 == 1 && tr.Prop2 != null) };
Option 2: SUBTHING approach.
You're looking at ALL SUBTHINGs and finding the ones where the condition is met, grouping by the ID at that point.
var result = from thing in search
from sub in thing.Subthings
where sub.Prop1 == 1 && sub.Prop2 != null
group sub by thing.id into sg
select new { ID = sg.Key, Names = sg.Select(tr => tr.Name) };
I like this approach just a little better, but still room for improvement. The reason I like this is because you find the SUBTHINGs first, and only then will it pull the THING that's associated with it (instead of first having to find if any SUBTHING matches the criteria, and THEN selecting it).
Option 3: Hybrid approach.
This is a little of both. We're going to select from SUBTHINGs either way, so might as well just perform the select. Then, if any of the projected subcollections have any elements, then we return our THING with the Names.
var result = from thing in search
let names = thing.Subthings
.Where(sub => sub.Prop1 == 1 && sub.Prop2 != null)
.Select(sub => sub.Name)
where names.Any()
select new { ID = thing.ID, Names = names };
Cleanest option, in my opinion. The Any extension method on a collection without any parameters will return true if there are any items in the collection. Perfect for our situation.
Hope that helps, let us know what you came up with.

IList comparison not behaving as expected

I have 2 lists that I am trying to compare, sadly they are not of the save class. One is from the magento api and the other is a custom class containing some values from the other. (only the ones that I need)
if (existingPhotoOrders.Where(x => x.OrderNumber.CompareTo(order.increment_id) == 0).ToList().Count > 0)
continue;
There is a problem with the API where I have to get all the orders and compare the ones that I want and then request individually extra information but the above query is not working it is just continueing with everything.
I have looked at other comparisons but they all require the classes to be the same. If anyone could shed some light I would be grateful.
It can be re-written like so using the Any method:
if (existingPhotoOrders.Any(x => x.OrderNumber.CompareTo(order.increment_id) == 0))
continue;
As to why it is always continuing, the expression must always be true! I presume the requirement is if the order increment id is not an order number of an existing order then continue, in which case, you can use the All method:
if (existingPhotoOrders.All(x => x.OrderNumber.CompareTo(order.increment_id) != 0))
continue;
Or as a more performant alternative:
var orderIds = new HashSet<int>(existingPhotoOrders.Select(epo => epo.OrderNumber));
foreach (var order in orders)
{
if (!orderIds.Contains(order.increment_id))
{
continue;
}
// rest of method here
}
The type of x and the type of order are not important, however the types of OrderNumber and increment_id. Of what types are they?
If one is string and the other int then you should convert the int to string:
.Where(x => x.OrderNumber == order.increment_id.ToString())

Categories