How to use DefaultIfEmpty in nested query - c#

I have this class:
class OriginalClass {
string FirstItem;
List<string> ListOfSecondItems;
}
I want to convert the list of one class into the list of another, or to "flatten" this class into new one:
class FlattenedClass {
string First;
string Second;
}
I'm using this LINQ expression to convert from one to another:
OriginalClass original;
var flattened = from Item in original
from secondItem in item.ListOfSecondItems
select new FlattenedClass(item.FirstItem, secondItem);
The problem is if list of second items is empty I lose the first item. I want to have some "(default)" value to be used if the list is null or empty. I guess DefaultIfEmpty is the key, but don't know how to incorporate it into existing query.

This call to DefaultIfEmpty says: "If that ListOfSecondItems is empty, use a single null instead."
var flattened =
from Item in original
from secondItem in item.ListOfSecondItems.DefaultIfEmpty()
select new FlattenedClass(item.FirstItem, secondItem);
Your question mentions the possibility that ListOfSecondItems might be null. This code handles that possibility. It also supplies a default string instead of null (using the other version of DefaultIfEmpty).
var flattened =
from Item in original
let subList = item.ListOfSecondItems ?? new List<string>()
from secondItem in subList.DefaultIfEmpty("(default)")
select new FlattenedClass(item.FirstItem, secondItem);

Related

C# - Cant convert var to List

This code snippet returns me an error,
public List<auto> autoSelect()
{
return autoSelect(DateTime.Today);
}
public List<auto> autoSelect(DateTime date)
{
var onderhoudAuto = (from onderhoud in db.onderhouds
where onderhoud.uitvoerdatum != DateTime.Today
select onderhoud)
.FirstOrDefault();
List<string> autos = (from auto in db.autos
where auto.autoId.Equals(onderhoudAuto)
select auto)
.FirstOrDefault();
return autos;
}
I tried convert the var to a list with .ToList(); although this doesn't work. Does anyone have any suggestions?
I tried convert the var to a list
No, you do not. var is not actually a data type - it is resolved by the compiler. A tooltip should show you the real type.
Your problem is different:
Looking at your code, we can see:
The method autoSelect signature states that the return type is List<auto>
public List<auto> autoSelect(DateTime date)
The variable autos type is List<string>
List<string> autos = [...etc...]
return autos;
You return autos, but it is not possible to return a List<string> when a List<auto> is expected.
So it has nothing to do with var - it is simply you selecting as single property and returning a list of strings, but that is not the type the method is supposed to return.
If you use FirstOrDefault() after your linq query, you are saying you want the first element (or the default -usually null- for the datatype if none matches) in the LINQ query, not a list of elements.
If you want a list of elements, use ToList() on the linq query, not try to convert a single entity to a list.
If you, for some reason, want a list of a single entity, then create a list (with new List<type>()) and then add your entity (of the same type as your list) to the list.

Using Linq to filter an array with bool properties true in to another array

I have an array of type Brick with each brick having an isBroken bool property. How can I use Linq to filter all bricks with isBroken = true into a new array?
Use Where to filter the list of bricks and ToArray to materialize the result into a new array.
var result = MyBricksArray.Where(x => x.isBroken).ToArray();
I hope this example will explain things more clearly, Let the definition of your class will be like the following:
public class Brick
{
public string Name;
public bool isBroken ;
}
And the Array of it's objects is defined like this:
Brick[] BrickArray =new Brick[]{
new Brick(){Name="A",isBroken=true},
new Brick(){Name="B",isBroken=true},
new Brick(){Name="C",isBroken=false}};
Then you can use the .Where to filter the collection like the following:
var selectedBriks = BrickArray.Where(x=>x.isBroken).ToList();
Now the selectedBriks will contains the items with name A and B
You can use the select method for this :
var theNewList = Brick.Where(b => b.isBroken).Select(b => new Brick
{
//Populate new object
}).ToList() / .ToArray();
Note that Select is used and not where to project the List into a new one.
Also, the .ToList()/ToArray() is to add the array to memory, you may not need it, .Select() return an IEnumerable.
It is possible to use Brik.Where(..).ToList(); as this would create a new List but the reference to the object inside the list would be the same, so again it depands on your need.
And .Select() require using System.Linq;

Why does concatenating with an empty list return a list but not when concatenating with new list?

I have a rather newb question:
Doing this
var emptyList = Enumerable.Repeat(Enumerable.Empty<int>(), 1).ToList();
var nonEmptyList = new List<int> { 1 };
var joinedList = emptyList.Select(x => x.Concat(nonEmptyList)).ToList();
returns a non empty list
However this return an empty list
var emptyList = new List<List<int>>();
var nonEmptyList = new List<int> { 1 };
var joinedList = emptyList.Select(x => x.Concat(nonEmptyList)).ToList();
How is Linq able to concatenate items from the empty list with the nonEmpty list when the selected items are empty lists themselves? and then why doesn't it work when I try it with a new list in the second example?
Thanks
Enumerable.Repeat returns an IEnumerable<IResult>. After calling ToList, it becomes a List<IResult>. What is TResult then? It is the type of the argument you passed to Repeat - IEnumerable<int>. So altogether, emptyList is a List<IEnumerable<int>>.
Now, is emptyList empty?
No, but the IEnumerable<int> inside it, is.
emptyList has an element. The element is an empty IEnumerable<int>. Why? Because you told it to Repeat an empty enumerable once. An "empty enumerable" does not mean nothing. Repeating an empty enumerable once gets you one empty enumerable, not nothingness.
After you understand this it's pretty clear what's happening here. You concatenated the empty enumerable to the 1 and that makes the joinedList to be {1}.
In the second case, new List<List<int>>() creates an empty list that has not lists in it, so Select does not do anything here.
EmptyList.Select iterates over an.... empty list. Hence, the result will also be empty.

Lambda Statements in Linq

Is it possible to do something like the following in Linq?
List<Group> groupsCollection = new List<Group>();
groups.Select( g => {
String id = g.Attributes["id"].Value;
String title = g.Attributes["title"].Value;
groupsCollection.Add( new Group(id, title) );
} );
This is just an example. I know a Foreach loop would be sufficient but I was querying whether it would be possible or not.
I tried it, and it's saying:
Cannot convert lambda expression to type System.Collections.IEnumerable<CsQuery.IDomObject> because it's not a delegate type
Edit: Group is my custom class. Groups are a collection of CsQuery Dom Objects. You can think of them as a collection of html anchor elements. They are IEnumerable.
I think you're looking for this:
groupsCollection = groups.Select(g =>
new Group(g.Attributes["id"].Value,
g.Attributes["title"].Value)).ToList();
Explanation:
Select() projects each element of a sequence into a new form. (From MSDN)
It's basically a transformation function that takes one IEnumerable and transforms it, in whatever way you like, to another IEnumerable, which seems to be exactly what you want here.
Select() takes a lambda expression that returns a value; it shouldn't have any sife effects.
You want
var groupsCollection = groups.Select(g => {
return ...;
}).ToList();
You can use the constructor to initialize it with the query:
var query = groups.Select( g => {
String id = g.Attributes["id"].Value;
String title = g.Attributes["title"].Value;
return new Group(id, title);
});
List<Group> groupsCollection = new List<Group>(query);
Do not modify colections in a linq-query, instead select data that can be used to modify collections.
As you said, you know using ForEach would be relevant. However you are just curious if you can do it another way. As others pointed out, you can use Select but the code in the {...} should return some value. However, you can do it this way which is better if you want to stop or break the loop depending on some condition using TakeWhile:
groups.TakeWhile( g => {
String id = g.Attributes["id"].Value;
String title = g.Attributes["title"].Value;
groupsCollection.Add( new Group(id, title) );
return true;//If you want to break the loop, just return false accordingly.
});

Contains functionality in Linq to XML Where Clause

I'm having unexpected behavior with the .Contains() function of the where clause in Linq to XML. It seems to be functioning like "==" not Contains() in the string function.
Example:
var q = from sr in SearchResults.Descendants("Result")
where _filters.Contains((string)sr.Element("itemtype"))
orderby (string)sr.Element("ipitemtype") ascending
select new SearchItem
{
//Create Object
ID = (string)sr.Element("blabla"),
}
_filters is a list of strings. Let's say it contains 3 values:
_filters[0] = "videos";
_filters[1] = "documents";
_filters[2] = "cat pictures";
What happens now, is that the Query works perfectly if
<itemtype>videos</itemtype>
is the XML node.
However, if the node is
<itemtype>videos mission critical document advertising</itemtype>,
the IEnumerable returns blank, which to me says the operand is functioning like "==" not "Contains()".
Any idea what I'm doing wrong?
Winning answer from dtb:
replace
where _filters.Contains((string)sr.Element("itemtype"))
with
where _filters.Any(filter => ((string)sr.Element("itemtype")).Contains(filter))
Try this:
_filters.Any(s => ((string)sr.Element("itemtype") ?? "").Contains(s))
This way you're checking that the element's value contains any of the strings in _filters. The use of the null coalescing operator ensures a NullReferenceException isn't thrown when the itemtype node doesn't exist since it is replaced with an empty string.
The other approach is to use let and filter out the nulls:
var q = from sr in SearchResults.Descendants("Result")
let itemtype = (string)sr.Element("itemtype")
where itemtype != null &&
_filters.Any(filter => itemtype.Contains(filter))
orderby (string)sr.Element("ipitemtype") ascending
select new SearchItem
{
//Create Object
ID = (string)sr.Element("blabla")
}
Note that String.Contains is case sensitive. So a check for "videos" won't match on "Videos" with a capital "V". To ignore case you can use String.IndexOf in this manner:
_filters.Any(filter => itemtype.IndexOf(filter, StringComparison.InvariantCultureIgnoreCase) >= 0)
You are checking if the array _filters has an element with the value "videos mission critial document advertising" (which is not the case), rather than if "videos mission critial document advertising" contains any of the elements in _filters.
Try this:
where _filters.Any(filter => ((string)sr.Element("itemtype")).Contains(filter))
The problem is that you are making false assumptions about the way the Contains method works. (Also see the String.Contains() documentation The contains method returns true if "a sequence contains a specific element". In the example you described, both the _filters array and the itemtype text contains the string videos, but neither contain each other. A more appropriate test would be to use the following extension method:
public static class ContainsAnyExtension
{
public static bool ContainsAny<T>(this IEnumerable<T> enumerable, params T[] elements)
{
if(enumerable == null) throw new ArgumentNullException("enumerable");
if(!enumerable.Any() || elements.Length == 0) return false;
foreach(var element in elements){
if(enumerable.Contains(element)){
return true;
}
}
return false;
}
}
So, your correct LINQ query would be:
var q = from sr in SearchResults.Descendants("Result")
where ((string)sr.Element("itemtype")).ContainsAny(_filters)
orderby ((string)sr.Element("ipitemtype")) ascending
select new SearchItem
{
ID = sr.Element("blabla").Value
};
It may also be helpful to review this post: How do I use LINQ Contains(string[]) instead of Contains(string), which is similar but specifically targets the String.Contains method instead of the Enumerable.Contains extension.

Categories