I'm new to Linq. I want to know whether this is the best way or are there any other ways to do this.
I have a requirement where from a web service, I receive a list of items:
class Item {
string ItemName { get; set;}
string GroupName { get; set; }
}
I receive the following data:
ItemName: Item1; GroupName: A
ItemName: Item2; GroupName: B
ItemName: Item3; GroupName: B
ItemName: Item4; GroupName: A
ItemName: Item5; GroupName: A
Now I want to get all of the unique Groups in the list, and associate all the Items to that Group. So I made a class:
class Group {
string GroupName { get; set; }
List<string> Items { get; set; }
}
So that there is a single group and all associated Items will be under the List.
I made two LINQ statements:
var uniqueGroups = (from g in webservice
where g.Group != null
select g.GroupName).Distinct();
Then I loop through it
foreach (var gn in uniqueGroups)
{
var itemsAssociated = (from item in webservice
where item.GroupName = gn.ToString()
select new {
});
}
and then I got the items, and save them to my object.
Is this the best way to do this or are there any LINQ statement that can do all these in one go?
Thanks.
Sounds like you want GroupBy
var itemsByGroup = items.GroupBy(i => i.GroupName);
foreach (var group in itemsByGroup)
{
var groupName = group.Key;
var itemsForThisGroup = group;
foreach (var item in itemsForThisGroup)
{
Console.Out.WriteLine(item.ItemName);
}
}
You can try this:
//List<Item> webservice = list with items from your webservice
var result = (from i in items
group i by i.GroupName into groups
select new Group()
{
GroupName = groups.Key,
Items = groups.Select(g => g.ItemName).ToList()
}).ToList();
I would use:
webservice.ToLookup(k => k.GroupName);
That would eliminate the need for the extra class.
Hope this helps!
That could be done all at once with an anonymous type and Enumerable.GroupBy:
var groupItems =
webservice.Where(i => i.GroupName != null)
.GroupBy(i => i.GroupName)
.Select(grp => new { Group = grp.Key, Items = grp.ToList() });
foreach (var groupItem in groupItems)
Console.WriteLine("Groupname: {0} Items: {1}"
, groupItem.Group
, string.Join(",", groupItem.Items.Select(i => i.ItemName)));
Distinct is useless since GroupBy will always make the groups distinct, that's the nature of a group.
Here's running code: http://ideone.com/R3jjZ
Related
Here's my LINQ query:
var results = from l in leads
from qr in l.QuoteRevisions
from rp in qr.RevisionProducts
select new QuoteSearchItem
{
ID = l.ID,
....
ProductCodes = l.QuoteRevisions.SelectMany(qr => qr.RevisionProducts)
.Select(p => p.Product.ProductCode).ToList()
};
The object we are trying to fill up looks like this:
public class QuoteSearchItem
{
public LeadID {get; set; }
....
public List<string> ProductCodes { get; set; }
....
}
The results I'm getting are almost accurate. But the problem is, when there is more than one product code, I get identical rows for each lead. So in stead of getting this:
{"LeadID": "12", "ProductCodes": ["Code1", Code2"]}
I get this:
{"LeadID": "12", "ProductCodes": ["Code1", Code2"]}
{"LeadID": "12", "ProductCodes": ["Code1", Code2"]}
So, I need to Group By l.LeadID. But I'm having trouble with that syntax. I tried this:
var results = from l in leads
from qr in l.QuoteRevisions
from rp in qr.RevisionProducts
group l by l.ID into lds
select new QuoteSearchItem
{
ID = lds.ID,
....
ProductCodes = lds.QuoteRevisions.SelectMany(qr => qr.RevisionProducts)
.Select(p => p.Product.ProductCode).ToList()
};
But then "lds" doesn't seen to contain anything. Not sure how to do this.
Thanks!
You are selecting all revision products and then constructing a list of leads. That is the problem, because now for every revision product you get one element containing all revision products for its lead.
Try removing subsequent from:
var results = from l in leads
select new QuoteSearchItem
{
ID = l.ID,
....
ProductCodes =
l.QuoteRevisions
.SelectMany(qr => qr.RevisionProducts)
.Select(p => p.Product.ProductCode)
.ToList()
};
List A: 3,5,5,5,7,9
List B: 3,5
Both of the list are the same type and those values are from a field ID. My objective is to construct a forloop that will return me 7,9 because 7,9 is not existed in List B.
I've tried the following but no luck:
int counter = 0;
foreach(var item in ListA.Where(x=>ListB.Any(b=>x.ID != b.ID)))
{
counter++;
//Here I should perform operation with item that having ID 7 and 9
}
Updates:
Using a except method in the above case, counter will still return me 4 simply because each of the 5 in ListA are different object eventhou they are sharing the same ID. My ultimate objective is to have the counter as 2 irregardless whether the object is the same or not. As long as the ID of object in ListA is 3 or 5, I would wanna exclude it.
Just use the Except extension mtehod
foreach (var item in ListA.Except(ListB)) {
...
}
it should be "ALL", or "Not Any"
foreach(var item in ListA.Where(x=>ListB.All(b=>x.ID != b.ID)))
{
//Here I should perform operation with item that having ID 7 and 9
}
update:
As you actually want to have distinct result from A except B, so, you can do either:
foreach(var item in ListA.GroupBy(m=>m.ID).Where(x=>ListB.All(b=>b.ID != x.Key)))
{
counter ++;
Debug.writeline(item.Key);
}
or
foreach(var id in ListA.Select(x=>x.ID).Distinct().Except(ListB.Select(y=>y.ID)))
{
counter++;
}
note: all untested - i have no compiler with me for the moment.
Change your query like this:
foreach(var item in ListA.Where(x=> !ListB.Any(b => x.ID == b.ID)))
And it should work fine.
Try This:
List<int> listA=new List<int>(new[]{ 3,5,7,9});
List<int> listB=new List<int>(new[]{ 3,5});
var items=(from a in listA
select a).Except(from b in listB
select b);
foreach(var item in items)
{
Console.WriteLine(ll);
}
Output:
7
9
Except method can be used when both List are of same type.
If Type is different. We can use like this.
var outPut = _employees.Where(i => _employeeExtensions.Any(j => i.EmpId == j.EmpId));
I think you want to get the items in a list where the items' IDs are different:
Example that I put together in LinqPad:
void Main()
{
List<Person> a = new List<Person>()
{
new Person { ID = 1 },
new Person { ID = 2 },
new Person { ID = 3 },
};
List<Person> b = new List<Person>()
{
new Person { ID = 1 },
};
var c = a.Where(x => b.Any(bprime => bprime.ID != x.ID));
foreach(var item in c)
{
Console.WriteLine(item.ID);
}
}
class Person
{
public int ID { get; set; }
}
Output:
2
3
This works similar to the Except method but this will check the elements' properties.
I'm trying to group a result set of ektron search results items and output them on screen, I use the following code
var groupdResults =
from result in response.Results
orderby result[SearchSmartFormProperty.GetDateProperty("/root/FundClosingDate")]
group result by result[SearchSmartFormProperty.GetStringProperty("/root/DeadlineAltText")]
into statusGroup
select new
{
closingDate =statusGroup.Key,
count= statusGroup.Count
};
I then add these to a listview: uxSearchResultView.DataSource = groupdResults;
The problem I'm having is that i need to output all the data from the resultset e.g. title,closingdate, etc.., It currently only outputs, e.g.
Closing 2
open 1
really appreciate any help anyone can offer
-----------------------Updated-------------------------------------
I think i have a working solution now, but its kind of messy
var groupdResults = from result in response.Results
orderby result[SearchSmartFormProperty.GetDateProperty("/root/FundClosingDate")]
group result by result[SearchSmartFormProperty.GetStringProperty("/root/DeadlineAltText")]
into statusGroup
select new
{
closingDate = statusGroup.Key,
count = statusGroup.Count(),
items = statusGroup.ToList()
};
List<Ektron.Cms.Search.SearchResultData> SRDATA = new List<Ektron.Cms.Search.SearchResultData>();
foreach (var result in groupdResults)
{
for (int i = 0; i < result.items.Count; i++)
{
SRDATA.Add(result.items[i]);
}
}
Any input as to a cleaner implementation?
thanks
You can do following:
select new
{
closingDate =statusGroup.Key,
count= statusGroup.Count(),
items = statusGroup.ToList()
};
items property will contain items that were assigned to that group in a List.
(Modified)
I think you need a class to capture your results:
class GroupResult
{
public string Status { get; set; }
public ICollection<SearchResultData> Items { get; set; }
}
The grouping statusGroup is an IEnumerable of your items, so you can do:
var groupdResults =
from result in response.Results
group result by result[SearchSmartFormProperty.GetStringProperty("/root/DeadlineAltText")]
into statusGroup
select new GroupResult
{
Status =statusGroup.Key,
Items = statusGroup.ToList()
}.ToList();
After that you can display the Items in any way you wish, like sorted by Status and the Items sorted by ClosingDate.
But maybe it is enough to just sort response.Results by status and then by closing date.
Rather than using anonymous types I would put that into a new object.
var groupdResults =
from result in response.Results
orderby result[SearchSmartFormProperty.GetDateProperty("/root/FundClosingDate")]
group result by result[SearchSmartFormProperty.GetStringProperty("/root/DeadlineAltText")]
into statusGroup
select new ResultObject
{
closingDate = statusGroup.First().ClosingDate,
....
};
I am trying to dynamically re-structure some data to be shown in a treeview which will allows the user to select up to three of the following dimensions to group the data by:
Organisation
Company
Site
Division
Department
So for example, if the user were to select that they wanted to group by Company then Site then Division...the following code would perform the required groupings.
var entities = orgEntities
// Grouping Level 1
.GroupBy(o => new { o.CompanyID, o.CompanyName })
.Select(grp1 => new TreeViewItem
{
CompanyID = grp1.Key.CompanyID,
DisplayName = grp1.Key.CompanyName,
ItemTypeEnum = TreeViewItemType.Company,
SubItems = grp1
// Grouping Level 2
.GroupBy(o => new { o.SiteID, o.SiteName })
.Select(grp2 => new TreeViewItem
{
SiteID = grp2.Key.SiteID,
DisplayName = grp2.Key.SiteName,
ItemTypeEnum = TreeViewItemType.Site,
SubItems = grp2
// Grouping Level 3
.GroupBy(o => new { o.Division })
.Select(grp3 => new TreeViewItem
{
DisplayName = grp3.Key.Division,
ItemTypeEnum = TreeViewItemType.Division,
}).ToList()
}).ToList()
})
.ToList();
This would give a structre like this:
+ Company A
+ Site A
+ Division 1
+ Division 2
+ Site B
+ Division 1
+ Company B
+ Site C
+ Division 2
+ Company C
+ Site D
However, this only provides me with on of a large number of combinations.
How would I go about converting this into something that could create the equivalent expression dynamically based on the three dimensions that the user has chosen and so I don't have to create one of each of these expressions for each combination!!?
Thanks guys.
An intriguing problem. Choosing a single type for grouping keys and another type for results... makes it is very possible to get what you're asking for.
public struct EntityGroupKey
{
public int ID {get;set;}
public string Name {get;set;}
}
public class EntityGrouper
{
public Func<Entity, EntityGroupKey> KeySelector {get;set;}
public Func<EntityGroupKey, TreeViewItem> ResultSelector {get;set;}
public EntityGrouper NextGrouping {get;set;} //null indicates leaf level
public List<TreeViewItem> GetItems(IEnumerable<Entity> source)
{
var query =
from x in source
group x by KeySelector(x) into g
let subItems = NextGrouping == null ?
new List<TreeViewItem>() :
NextGrouping.GetItems(g)
select new { Item = ResultSelector(g.Key), SubItems = subItems };
List<TreeViewItem> result = new List<TreeViewItem>();
foreach(var queryResult in query)
{
// wire up the subitems
queryResult.Item.SubItems = queryResult.SubItems
result.Add(queryResult.Item);
}
return result;
}
}
Used in this way:
EntityGrouper companyGrouper = new EntityGrouper()
{
KeySelector = o => new EntityGroupKey() {ID = o.CompanyID, Name = o.CompanyName},
ResultSelector = key => new TreeViewItem
{
CompanyID = key.ID,
DisplayName = key.Name,
ItemTypeEnum = TreeViewItemType.Company
}
}
EntityGrouper divisionGrouper = new EntityGrouper()
{
KeySelector = o => new EntityGroupKey() {ID = 0, Name = o.Division},
ResultSelector = key => new TreeViewItem
{
DisplayName = key.Name,
ItemTypeEnum = TreeViewItemType.Division
}
}
companyGrouper.NextGrouping = divisionGrouper;
List<TreeViewItem> oneWay = companyGrouper.GetItems(source);
companyGrouper.NextGrouping = null;
divisionGrouper.NextGrouping = companyGrouper;
List<TreeViewItem> otherWay = divisionGrouper.GetItems(source);
Another option is to use DynamicLinq. If this is straight LINQ (not through some DB context such as LINQ2SQL), then this can be done by composing your grouping/selector strings:
var entities = orgEntities
.GroupBy("new(CompanyID, CompanyName)", "it", null) // DynamicLinq uses 'it' to reference the instance variable in lambdas.
.Select(grp1 => new TreeViewItem
{
...
.GroupBy("new(SiteID, o.SiteName)", "it", null)
// And so on...
You can probably abstract this into each of the criteria type. The only issue I see is the inner groupings might not be the easiest to compile together, but at least this can get you started in some direction. DynamicLinq allows you to build dynamic types, so it's certainly possible to abstract it even further. Ultimately, the biggest challenge is that based on what you're grouping by, the generated TreeViewItem contains different information. Good use case for dynamic LINQ, but the only problem I see is abstracting even further down the line (to the inner groupings).
Let us know what you come up with, definitely an interesting idea that I hadn't considered before.
Assuming I had a collection like this..
var list = new List<Item>
{
new Item
{
Name = "Software",
Price = 100
},
new Item
{
Name = "Software",
Price = 200
},
new Item
{
Name = "Hardware",
Price = 100
}
};
And the 'Names' are not going to be known, I want to write a LINQ query that will return a list of everything with a matching name. I cannot use "Select", because the names, again, are not known at design time.
Any ideas?
I'm not entirely sure whether you want to filter, or group the results.
If you want to filter, you can use Where with a runtime-supplied name:
string nameToFind = GetTheNameToFind(); // Can come from user, config, etc
var matches = list.Where(item => item.Name == nameToFind);
If you want to group by all of the names (ie: have 2 software + 1 hardware element), you could use Enumerable.GroupBy:
var groups = list.GroupBy(item => item.Name);
foreach(var group in groups)
{
Console.WriteLine("Group {0}:", group.Key);
foreach(var item in group)
Console.WriteLine(" Item: {0} - {1}", item.Name, item.Price);
}
Are you looking for this? You can use Where method to filter enumerable.
name variable can be defined at runtime.
string name = "Software";
List<Item> newList = list.Where(item => item.Name == name).ToList();