SelectMany Three Levels Deep - c#

I can flatten the results of a child collection within a collection with SelectMany:
// a list of Foos, a Foo contains a List of Bars
var source = new List<Foo>() { ... };
var q = source.SelectMany(foo => foo.Bar)
.Select(bar => bar.barId)
.ToList();
this gives me the list of all Bar Ids in the Foo List. When I attempt to go three levels deep the incorrect result is returned.
var q = source.SelectMany(foo => foo.Bar)
.SelectMany(bar => bar.Widget)
.Select(widget => widget.WidgetId)
.ToList();
How should I be using SelectMany to get the list of all Widgets in all Bars in my list of Foos?
Edit
I miss-worded the above sentence, but the code reflects the goal. I am looking for a list of all Widget Ids, not widgets.
An "incorrect" result is not all of the widget ids are returned.

Your query is returning all the widget IDs, instead of all the widgets. If you just want widgets, just use:
var q = source.SelectMany(foo => foo.Bar)
.SelectMany(bar => bar.Widget)
.ToList();
If that's still giving "the incorrect result" please explain in what way it's the incorrect result. Sample code would be very helpful :)
EDIT: Okay, if you want the widget IDs, your original code should be fine:
var q = source.SelectMany(foo => foo.Bar)
.SelectMany(bar => bar.Widget)
.Select(widget => widget.WidgetId)
.ToList();
That could also be written as
var q = (from foo in source
from bar in foo.Bar
from widget in bar.Widget
select widgetId).ToList();
if you like query expression format.
This really should work - if it's not working, that suggests there's something wrong with your data.
We should have checked before - is this just LINQ to Objects, or a fancier provider (e.g. LINQ to SQL)?

var q = (
from f in foo
from b in f.Bars
from w in b.Widgets
select w.WidgetId
).ToList();
Also note that if you want the unique list, you can do .Distinct().ToList() instead.

var q = source.SelectMany(foo => foo.Bar)
.SelectMany(bar => bar.Widget,(bar,widget) => widget.WidgetId)
.ToList();
we can call this overload of SelectMany() with allow us to specify the projection using lambda experession

Related

C# Linq union multiple properties to one list

Basically I have an object with 2 different properties, both int and I want to get one list with all values from both properties. As of now I have a couple of linq queries to do this for me, but I am wondering if this could be simplified somehow -
var componentsWithDynamicApis = result
.Components
.Where(c => c.DynamicApiChoicesId.HasValue ||
c.DynamicApiSubmissionsId.HasValue);
var choiceApis = componentsWithDynamicApis
.Select(c => c.DynamicApiChoicesId.Value);
var submissionApis = componentsWithDynamicApis
.Select(c => c.DynamicApiSubmissionsId.Value);
var dynamicApiIds = choiceApis
.Union(submissionApis)
.Distinct();
Not every component will have both Choices and Submissions.
By simplify, I assume you want to combine into fewer statements. You can also simplify in terms of execution by reducing the number of times you iterate the collection (the current code does it 3 times).
One way is to use a generator function (assuming the type of items in your result.Components collection is Component):
IEnumerable<int> GetIds(IEnumerable<Component> components)
{
foreach (var component in components)
{
if (component.DynamicApiChoicesId.HasValue) yield return component.DynamicApiChoicesId.Value;
if (component.DynamicApiSubmissionsId.HasValue) yield return component.DynamicApiSubmissionsId.Value;
}
}
Another option is to use SelectMany. The trick there is to create a temporary enumerable holding the appropriate values of DynamicApiChoicesId and DynamicApiSubmissionsId. I can't think of a one-liner for this, but here is one option:
var dynamicApiIds = result
.Components
.SelectMany(c => {
var temp = new List<int>();
if (c.DynamicApiChoicesId.HasValue) temp.Add(c.DynamicApiChoicesId.Value);
if (c.DynamicApiSubmissionsId.HasValue) temp.Add(c.DynamicApiSubmissionsId.Value);
return temp;
})
.Distinct();
#Eldar's answer gave me an idea for an improvement on option #2:
var dynamicApiIds = result
.Components
.SelectMany(c => new[] { c.DynamicApiChoicesId, c.DynamicApiSubmissionsId })
.Where(c => c.HasValue)
.Select(c => c.Value)
.Distinct();
Similar to some of the other answers, but I think this covers all your bases with a very minimal amount of code.
var dynamicApiIds = result.Components
.SelectMany(c => new[] { c.DynamicApiChoicesId, c.DynamicApiSubmissionsId}) // combine
.OfType<int>() // remove nulls
.Distinct();
To map each element in the source list onto more than one element on the destination list, you can use SelectMany.
var combined = componentsWithDynamicApis
.SelectMany(x => new[] { x.DynamicApiChoicesId.Value, x.DynamicApiSubmissionsId.Value })
.Distinct();
I have not tested it but you can use SelectMany with filtering out the null values like below :
var componentsWithDynamicApis = result
.Components
.Select(r=> new [] {r.DynamicApiChoicesId,r.DynamicApiSubmissionsId})
.SelectMany(r=> r.Where(p=> p!=null).Cast<int>()).Distinct();

How to do in this in Linq C#

So far, I have this:
var v = Directory.EnumerateFiles(_strConfigurationFolder)
.GroupBy(x => GetReportName(Path.GetFileNameWithoutExtension(x)));
Configuration folder will contain pairs of files:
abc.json
abc-input.json
def.json
def-input.json
GetReportName() method strips off the "-input" and title cases the filename, so you end up with a grouping of:
Abc
abc.json
abc-input.json
Def
def.json
def-input.json
I have a ReportItem class that has a constructor (Name, str1, str2). I want to extend the Linq to create the ReportItems in a single statement, so really something like:
var v = Directory.EnumerateFiles(_strConfigurationFolder)
.GroupBy(x => GetReportName(Path.GetFileNameWithoutExtension(x)))
**.Select(x => new ReportItem(x.Key, x[0], x[1]));**
Obviously last line doesn't work because the grouping doesn't support array indexing like that. The item should be constructed as "Abc", "abc.json", "abc-input.json", etc.
If you know that each group of interest contains exactly two items, use First() to get the item at index 0, and Last() to get the item at index 1:
var v = Directory.EnumerateFiles(_strConfigurationFolder)
.GroupBy(x => GetReportName(Path.GetFileNameWithoutExtension(x)))
.Where(g => g.Count() == 2) // Make sure we have exactly two items
.Select(x => new ReportItem(x.Key, x.First(), x.Last()));
var v = Directory.EnumerateFiles(_strConfigurationFolder)
.GroupBy(x => GetReportName(Path.GetFileNameWithoutExtension(x))).Select(x => new ReportItem(x.Key, x.FirstOrDefault(), x.Skip(1).FirstOrDefault()));
But are you sure there will be exactly two items in each group? Maybe has it sence for ReportItem to accept IEnumerable, not just two strings?

Passing a list<string> into Linq

The first query returns a list of string, and I am passing them into another table to find corresponding items, but nothing happens. no error message or nothing
var classIds = _contextSpecRepo.Get(x => x.cId.Equals(cId)).Select(x => x.classNames).Distinct().ToList();
// issue happens in the following query
var classes= Repository.Get(x => x.Id.Equals(classIds)).ToList();
The call to Equals, which takes object, hides the problem: you are comparing a single Id to a list of Ids, rather than checking if the Id is present in a collection. This compiles, but yields no result.
Here is how you can fix it:
var classes= Repository.Get(x => classIds.Any(y => y == x.Id)).ToList();
or
var classes= Repository.Get(x => classIds.Contains(x.Id)).ToList();
If you must do it in 2 queries then you have to use contains
var classes= Repository.Get(x => classIds.Contains(x.Id)).ToList();
A better solution would be to use a join on the tables.
you can also skip .ToList()
var classes= Repository.Get(x => classIds.Contains(x.Id));

Query an XML using LINQ and excluding where an Attribute value is equal to that of an Element

I have a LINQ query against an XML, that gives me a list of nested lists, each sublist being a list of an elements("row") attributes.
var items = loadbodies.Descendants("row").Select(a => a.Attributes().Select(b => b.Value).ToList()).ToList();
This works as intended but, what I actually need to is query this against another list of values so as not to have sublists added where one of the elements attributes("messageID") is on the second list. I can do this for one value but need to check it against the entire second list.
The query to exclude a single sublist by a single hardcoded value from the second list is below.
var items = loadbodies.Descendants("row").Where(c => (string)c.Attribute("messageID") != "avaluefromthesecondlist").Select(a => a.Attributes().Select(b => b.Value).ToList()).ToList();
Any help would be much appreciated.
Just use Contains. Note that splitting lines helps readability considerably:
var ids = ...; // Some sequence of ids, e.g. a List<string> or HashSet<string>
var items = loadbodies
.Descendants("row")
.Where(row => ids.Contains((string) row.Attribute("messageId")))
.Select(a => a.Attributes()
.Select(b => b.Value)
.ToList())
.ToList();
Note that you could use a Join call too... but so long as you've got relatively few IDs, this should be fine.

Why does .Equals not work in this LINQ example?

Why does this yield an empty set?
Object[] types = {23, 234, "hello", "test", true, 23};
var newTypes = types.Select(x => x.GetType().Name)
.Where(x => x.GetType().Name.Equals("Int32"))
.OrderBy(x => x);
newTypes.Dump();
When you do your select you're getting an IEnumerable<String>. Then you're taking the types of each string in the list (which is all "String") and filtering them out where they aren't equal to "Int32" (which is the entire list). Ergo...the list is empty.
Equals works just fine, it's your query that isn't correct. If you want to select the integers in the list use:
var newTypes = types.Where( x => x.GetType().Name.Equals("Int32") )
.OrderBy( x => x );
Reverse the order of the operations:
var newTypes = types.Where(x => x is int)
.OrderBy(x => x)
.Select(x => x.GetType().Name);
(Notice this also uses a direct type check instead of the rather peculiar .GetType().Name.Equals(…)).
The thing with LINQ is you've got to stop thinking in SQL terms. In SQL we think like this:-
SELECT Stuff
FROM StufF
WHERE Stuff
ORDER BY Stuff
That is what your code looks like. However in LINQ we need to think like this :-
FROM Stuff
WHERE Stuff
SELECT Stuff
ORDER BY Stuff
var newTypes = types.Select(x => x.GetType().Name)
.Where(x => x.Equals("Int32"))
.OrderBy(x => x);
This doesn't work because the Select statement will convert every value in the collection to the name of the underlying type of that value. The resulting collection will contain only string values and hence they won't ever have the name Int32.

Categories