I am trying to use LINQ to query a list of objects wherever appropriate. Currently I am stuck on the syntax of a nested query which I hope you can help me with.
Classes:
public class FooType
{
public int Id { get; set; }
public IList<Foo> Foos { get; set; }
}
public class Foo
{
public int FooTypeId { get; set; }
public string CorrelationId { get; set; }
}
public class Bar
{
public string FooCorrelationId { get; set; }
}
Usage:
IList < FooType > fooTypes = new List < FooType >();
// ... add a lot of FooTypes, each enriched with some Foos
Bar bar = new Bar(){FooCorrelationId = "abcdef"};
Foo foo = fooTypes.Where( ft => ft.Foos.Where( f => f.CorrelationId == bar.FooCorrelationId ) ).First<Foo>();
This fails because the outer WHERE-expression is fed with something that does not provide a boolean return value. As you might guess, I intend to something like the following in LINQ:
foreach (FooType fooType in fooTypes)
{
Foo foo = fooType.Foos
.Where(f => f.CorrelationId == bar.FooCorrelationId)
.First();
}
Do you have any idea how to get that Foo I'm looking for, the one with CorrelationId "abcdef"?
And one more question for the benchmarkers : What about performance? Does LINQ optimize it's queries? Or is the result the same as an iteration over the objects like I did in my "something like" block?
Thanks a lot in advance!
Just replace the outer where with a SelectMany and get the first
Foo foo = fooTypes
.SelectMany(ft => ft.Foos.Where(f => f.CorrelationId == bar.FooCorrelationId))
.First();
If you an to get the first Foo across all FooType, what has CorrelationId == FooCorrelationId.
Related
I have a model like so
public class UserModel
{
List<UserModel> users
}
public class UserModel
{
public List<UserSomeObj> userSomeObj { get; set; }
public List<UserSomeOtherObj> userSomeOtherObj { get; set; }
}
public class UserSomeObj
{
public int someIntProperty { get; set; }
public string someStringProperty { get; set; }
}
public class UserSomeOtherObj
{
public int someIntProperty { get; set; }
public string someStringProperty { get; set; }
}
Each UserModel class List is comprised of several other class Lists.
I am referencing them dynamically like so by looping over a list of targeted properties.
to get a list of properties matching the 'prop' variable;
var props = MethodToGetTargetedProperties();
// props example content would be a list of strings like so "UserSomeObj", "UserSomeOtherObj"
foreach (var prop in props)
{
var results = users.Select(x => x.GetPropertyValue(prop)).ToList();
//results contain lists of prop where count == 0 and i dont want them
}
what I am trying to do is reduce the results where count of the lists targeted is greater than 0 .... problem is that I can't find the correct order/syntax to get it to work.
Thanks
As List<T> implements the non-generic ICollection interface, you can cast to that:
var results = users.Select(x => x.GetPropertyValue(prop))
.Cast<ICollection>()
.Where(list => list.Count > 0)
.ToList();
You could do the cast within the Where if you want, although I prefer the above:
var results = users.Select(x => x.GetPropertyValue(prop))
.Where(list => ((ICollection) list).Count > 0)
.ToList();
Thanks ..... in reviewing your reply I was able to do the following
var results = users.Select(x => x.GetPropertyValue(prop))
.Cast<IEnumerable<object>>().Where(y => y.Count() > 0).ToList();
Then after looking at your answer more and trying to understand all its facets, I wondered if I could do it all in one line. The two List classes in UserModel have 2 common properties (idSomething and idSomeOtherThing) and since I will be combining them and doing a Distinct, i thought, hmm, one liner might be possible.
I have a list of foo called crepes. I want to return foo where bar.doritos == "coolRanch"
class foo
{
List<bar> item;
string candy;
string beer;
}
class bar
{
string doritos;
string usb;
}
var item = crepes.item.Where(x => x.doritos == "coolRanch").FirstOrDefault();
From other threads, i've pieced together the above linq query, but crepes.item throws an error. "List does not contain a definition for 'item' and no definition for 'item' accepting first argument...
Given that crepes is a List<Foo>, you need to add an additional level to the linq query.
var item = crepes.Where(a => a.item.Any(x => x.doritos == "coolRanch")).FirstOrDefault();
Your item access modifier is private (this is C# default for class), it should be made public
This goes for your doritos too
Also, since your crepes is a List, put additional layer of LINQ (as also suggested by others) to completely fix it, something like this
var item = crepes.Where(f => f.item.Any(b => b.doritos == "coolRanch")).FirstOrDefault(); //f is of foo type, b is of bar type
If you fix your classes like this
class Foo
{
public List<Bar> Items { get; set; }
public string Candy { get; set; }
public string Beer { get; set; }
}
class Bar
{
public string Doritos { get; set; }
public string Usb { get; set; }
}
Your query will look like
var crepes = new List<Foo>();
var item = crepes.FirstOrDefault(f => f.Items.Any(b => b.Doritos == "coolRanch"));
Here, we are trying to get the first Foo which has at least one Bar in Items where Doritos == "coolRanch".
I have the following classes:
public class ClassA(){
public int Id { get; set; }
public ICollection<ClassB> ClassBs{ get; set; }
}
public class ClassB(){
public int Id { get; set; }
public int ClassAId{ get; set; }
public int ClassCId{ get; set; }
public virtual ClassA ClassA{ get; set; }
public virtual ClassC ClassC{ get; set; }
}
public class ClassC(){
public int Id { get; set; }
public string Name{ get; set; }
public ICollection<ClassB> ClassBs{ get; set; }
}
Now, I have a list of ClassC objects, that comes from the user`s selection.
Let's say the user chose the ClassC's of Ids 1 and 3. I need to return a list of all ClassA objects meets the criteria.
A general rule for situations when you have a collection of collections, and you wish to return elements of the collection on the inside, is to use SelectMany before doing further processing. In your case this would look as follows:
var res = collectionOfCs
.Where(cItem => ... /* Condition on ClassC */)
.SelectMany(cItem => cItem.ClassBs.Select(bItem => bItem.ClassA))
.Where(aItem => ... /* Condition on ClassA */)
You could separate selection of ClassAs from SelectMany, but the overall structure of the query would remain the same:
var res = collectionOfCs
.Where(cItem => ... /* Condition on ClassC */)
.SelectMany(cItem => cItem.ClassBs)
.Select(bItem => bItem.ClassA) // Moved from SelectMany
.Where(aItem => ... /* Condition on ClassA */)
You can use SelectMany extension method:
var query = listOfClassC.Where(c => c.Id == 1 || c.Id == 3)
.SelectMany(c => c.ClassBs.Select(b => b.ClassA));
Now, to filter better the objects of type ClassC, you can also do this using the Contains method:
var ClassCIds=new List<int>(){1,3}; // Add here the ids what you want to filter
var query = listOfClassC.Where(c => ClassCIds.Contains(c.Id))
.SelectMany(c => c.ClassBs.Select(b => b.ClassA));
listOfObjectsOfClassA.Where(a => a.ClassBs
.Where(b => listOfUserProvidedIds.Contains(b.ClassCId))
.Count() > 0)
.ToList()
That should do what you're asking for, unless I've missunderstood something.
What that does is, take the list of ClassA objects, goes through the ClassB list that is inside every ClassA object and checks if the list of user provided IDs (in your example that list would contain 1and 3) contains the ClassCId that is assigned to the specific ClassB object.
I have a problem:
In my code I have this obj
//:extension for being saved locally
public class FOO
{
public int ID { get; set; }
public string STRINGVALUE{ get; set; }
public int INTVALUE { get; set; }
//ecc..
//SelectAll() method which returns all FOO in local DB
}
Now what I want is, in some cases, to pick all FOO obj where STRINGVALUE (or INTVALUE) is not duplicated.
For example:
List<FOO> fooes = new FOO().SelectAll();
List<FOO> uniqueIntFoo = fooes.distinct(); //here i have to set the clause
There is the Distinct() method in Linq, but it compare the entire item, not a single variable of it.
Any of you know how can I do it?
What about grouping the results, something like:
By both Int and StrValue:
var uniqueItems = fooes.GroupBy(x => new{x.IntVal, x.StrVal});
By IntVal:
var uniqueItems = fooes.GroupBy(x => x.IntVal);
Edit, as suggested by question's author:
fooes.GroupBy(x => x.INTVALUE).Select(y => y.First()).Distinct().ToList();
I would like to merge two Lists of Lists of a certain class that share the same key
Let's say that I have the class :
public class Album {
public string Name { get ; set; }
public string Genre{ get ; set; }
}
and two Lists of Lists :
public List<List<Album>> AlbumList1 ;
public List<List<Album>> AlbumList1 ;
I would like to merge the lists in AlbumList1 and AlbumList2 that have the same Key .
For example if a List is called "Genre1" and another is called "Genre1" i would like to merge those two lists to create a unique list .
How can I perform this ?
It would probably look a bit like this:
var results = albumList1
.SelectMany(l => l)
.Concat(albumList2.SelectMany(l => l)
.GroupBy(l => l.Name, g => g.ToList())
.ToList();
Or perhaps like this:
var results = albumList1
.Join(albumList2,
l => l[0].Name,
l => l[0].Name,
(l1, l2) => l1.Concat(l2).ToList())
.ToList();
However, I'd also recommend you consider refactoring the code to use a IDictionary<string, IEnumerable<Album>> or an ILookup<string, Album> instead.
Seems like you might want to refactor that outer list a bit out to a class. Something like this:
public class Genre
{
public string Name { get; set; }
public List<Album> Albums { get; set; }
}
public class Album
{
public string Genre { get; set; }
public string Name { get; set; }
}
After that, you can then create a comparer (simplified)
public class GenreComarer : IEqualityComparer<Genre>
{
public bool Equals(Genre x, Genre y)
{
return x.Name.Equals(y.Name);
}
public int GetHashCode(Genre obj)
{
return obj.Name.GetHashCode();
}
}
public class AlbumComarer : IEqualityComparer<Album>
{
public bool Equals(Album x, Album y)
{
return x.Name.Equals(y.Name);
}
public int GetHashCode(Album obj)
{
return obj.Name.GetHashCode();
}
}
Then, it's a simple join - adding back in the missing Genres.
List<Genre> unified = list1.Join(list2,
e => e.Name,
e => e.Name,
(genre1, genre2) => new Genre
{
Name = genre1.Name,
Albums = genre1.Albums.Union(genre2.Albums, new AlbumComarer()).ToList()
}
).ToList();
unified.AddRange(list2.Except(list1, new GenreComarer()));
unified.AddRange(list1.Except(list2, new GenreComarer()));