Parent and Child sorting using Lambda expression in C# - c#

I'm currently working on a project and working some lambda expressions (basics). And I have this requirement that I need to display the data based on hierarchy (parent and child).
Current Class:
public class Phones
{
public int ID { get; set; }
public string Phone { get; set; }
public int ChildOf { get; set; }
}
Current Data
ID Phone ChildOf
1 Samsung 0
2 Apple 0
3 GalaxyS3 1
4 GalaxyS4 1
5 5S 2
6 iPhone 6+ 2
I'm expecting a result of:
Results
**Samsung**
GalaxyS3
GalaxyS4
**Apple**
5S
iPhone6+

You can do this using the linq as below.
var result = phoneList.Where(x=>x.ChildOf!=0).GroupBy(x=>x.ChildOf).Select(g=> new {Key = phoneList.FirstOrDefault(x=>x.ChildOf==0 && x.ID ==g.Key).Phone,Value = g.Select(x => x.Phone)});
But mind you phoneList.FirstOrDefault(x=>x.ChildOf==0) this is happening just because you want the Phone Name for the grouping Key.
You can do the separate dictionary to get the values and use it. But if you have the parent ID then why use the name. You can use the parent ID whenever you required from the group key. IF you are to do this,
var result = phoneList.Where(x=>x.ChildOf!=0).GroupBy(x=>x.ChildOf).Select(g=> g.Key,g.ToList());
And when you need to show that on the UI Just replace the Key with the matching parent ID. That would reduce the lot of performance cost.

Would something like this suffice:
var lookup = phoneData.Where(e => e.ChildOf == 0)
.ToDictionary(id => id.Id, value => value.Phone);
var result = phoneData.Where(e => e.ChildOf > 0)
.GroupBy(e => lookup[e.ChildOf])
.Select(g => new { Parent = g.Key, Children = g.Select(x => x.Phone) });
Where phoneData is an enumerable of your raw data.

Related

Receiving search results with grouping by doc type in one call (NEST, AWS Elasticsearch)

all. Thanks in advance for your help!
I'm requesting paged data from AWS Elasticsearch using NEST. Each document in AWS Elasticsearch index has a content type (Topic, Question and Video). In response I receive a list of documents for the current page and their total results. Everything is ok with it.
So the question is: how can I receive some kind of response map as well as search results in one call?
I mean, for example, here is my response:
Doc 1 - Topic
Doc 2 - Topic
Doc 3 - Video
Doc 4 - Question
Doc 5 - Video
Total results: 5 items.
In addition to this, I'd like to receive the following 'map':
Topic - 2 items
Video - 2 items
Question - 1 item
Is it possible to do in one request? Or how this can be done by several requests? Maybe NEST Aggregations is something of the needed solution, but it seems that do not have 'count' logic
This is the search request for paged data and some models:
public class Document
{
public int DocumentId { get; set; }
public ContentType ContentType { get; set; }
public DateTime UpdatedOn { get; set; }
public string Title { get; set; }
public string Description { get; set; }
}
public virtual DocumentSearchResponse FullTextSearch(DocumentSearchParams searchParams)
{
var resultsSearchRequest = _elasticClient.Search<Document>(s => s.Index("some_index")
.Query(q => q.Term(t => t.Field(f => f.DocumentId).Value(searchParams.ContentId))
&& q.Terms(t => t.Field(f => f.ContentType).Terms(searchParams.GetContentTypesIds()))
&& q.MultiMatch(m => m.Fields(fs => fs.Field(f => f.Title).Field(f => f.Description))
.Query(searchParams.SearchValue)
.Type(TextQueryType.MostFields)))
.Sort(ss => ss.Descending(f => f.UpdatedOn))
.From((searchParams.PageNumber - 1) * searchParams.PageSize)
.Size(searchParams.PageSize));
// Is valid check
return new DocumentSearchResponse
{
PageResults = _searchResponseHelper.ToPageResults(resultsSearchRequest.Documents),
PageResultsMap = new Dictionary<ContentType, int>, // <- here
TotalResultsCount = resultsSearchRequest.HitsMetadata.Total.Value
};
}
So, I've found it out. Maybe, someone will find it helpful.
The thing I've needed are 'Aggregations'. Adding the following row before .Form and .Size will group the search results by the specified field:
.Aggregations(a => a.Terms("contentType", t => t.Field(f => f.ContentType)))
.Query(...)
.Form(...)
.Size(...)
To process the result to the Dictionary I need, add the following (
searchAggregations is searchResponse.Aggregations):
public Dictionary<ContentType, long> ToPageResultsMap(AggregateDictionary searchAggregations)
{
var pageResultsMap = new Dictionary<ContentType, long>();
var buckets = searchAggregations.Terms(DataConsts.SearchContentTypeDocFieldName).Buckets;
foreach (var item in buckets)
{
if(int.TryParse(item.Key, out int contentType))
{
pageResultsMap.Add((ContentType)contentType, item.DocCount.HasValue ? item.DocCount.Value : 0);
}
}
return pageResultsMap;
}

Group objects list by Dictionary value

I've list of object which looks like below
public class test
{
public int ID{ get; set; }
public string Name { get; set; }
public Dictionary<string, string> SampleXML { get; set; }
}
I want to group the list based on values in 'SampleXML'. e.g. It contains values like price. I want to group List based on price.
How to implement the same. I tried below code but it just seperates the list by key. I need unique records.
Dictionary<string, List<test>> result = Obj.LstTest
.GroupBy(x => x.SampleXML["Price"])
.ToDictionary(g => g.Key,
g => g.ToList());
The above code generates the list based on price. e.g. If there are 6 records with 3 different prices, above code return 3 results,but again the list contains two records each.
EDIT
If the price is blank then it needs to be ignored in grouping that is if there are 3 records and all doesn't have prices then list will contain 3 item(as it is).
Any help is appreciated.
Edit: Based on Vicky S edit
To still have the result as a List of test:
List<test> list = new List<test>();
List<test> result = new List<test>();
result = list.GroupBy(x => x.SampleXML["Price"]).Select(g=>g.FirstOrDefault());
Edit: To include tests that have SampleXML with empty Price value or no Price Key. Do the following:
First we need to separate tests with "No Price" and include them to our final result, from test that have "Price" value to group them.
var emptyPrice = list.Where(l => !l.SampleXML.ContainsKey("Price") || l.SampleXML["Price"] == string.Empty).ToList();
var withoutEmptyPrice = list.Where(l => l.SampleXML.ContainsKey("Price") && !string.IsNullOrEmpty(l.SampleXML["Price"]));
Then we will group tests with "Price" value as my first answer.
var resultWithEmptyPrice = withoutEmptyPrice.GroupBy(x => x.SampleXML["Price"]).Select(g => g.FirstOrDefault()).ToList();
Finally, we will add the "No Price" tests to our result.
resultWithEmptyPrice.AddRange(emptyPrice);

Build Linq queries and concatenate dynamically based on a table

I'm summarizing data to build an alert system. This query counts how many points an employee has based on some criteria. The one below is working fine. However, the query can be longer depending on the User's location. For example, below I have only have 3 unions but for other Employees' locations more unions may be needed. For example in Location B they may also want to add an alert if a ReasonTypeID == is 3 and any entries in the last 90 days.
So I was thinking on building a table that I can add parameters on a location by location basis. I've read about dynamic linq library and I can use that for the WHERE statements but how would I add another query concatenation on the fly?
Here is what I have now.
public class TMAlert
{
public string EmpID { get; set; }
public string FullName { get; set; }
public decimal? PointSummary { get; set; }
public string WarningLabel { get; set; }
public bool AlertFlag { get; set; }
}
IEnumerable<TMAlert> tmAlert = (
from a in allEntries
where a.Date >= DateTime.Now.AddDays(-30) && a.ReasonTypeID == 1
group a by new
{
a.EmpID,
a.FullName,
a.ReasonTypeID
} into g
select new TMAlert
{
EmpID = g.Key.EmpID,
FullName = g.Key.FullName,
WarningLabel = "Last 30 Days",
PointSummary = g.Sum(a => a.Points),
AlertFlag = (g.Sum(a => a.Points) >= 4) ? true : false
}).Concat(from a in allEntries
where a.Date >= DateTime.Now.AddDays(-90) && a.ReasonTypeID == 1
group a by new
{
a.EmpID,
a.FullName,
a.ReasonTypeID
} into g
select new TMAlert
{
EmpID = g.Key.EmpID,
FullName = g.Key.FullName,
WarningLabel = "Last 90 Days",
PointSummary = g.Sum(a => a.Points),
AlertFlag = (g.Sum(a => a.Points) >= 9) ? true : false
}).Concat(from a in allEntries
where a.Date >= (
from o in allEntries
where o.EmpID == a.EmpID && a.WarningTypeID == 2
select (DateTime?)o.Date).Max()
group a by new
{
a.EmpID,
a.FullName,
a.ReasonTypeID
} into g
select new TMAlert
{
EmpID = g.Key.EmpID,
FullName = g.Key.FullName,
WarningLabel = "60 Since Warning type 2 ",
PointSummary = g.Sum(a => a.Points),
AlertFlag = (g.Sum(a => a.Points) >= 4) ? true : false
});
I was thinking to build a criteria table then build the queries on the fly based on the table.
for example
for each entry in criteriaTable
add dynamic linq entry.Param
.concat(....
next....
is this possible or are there better ways to do this?
There is a way to combine linq queries dynamically. I use it to add conditional filters to my where statements when showing a filtered list of data.
I initialize the lambda expression like this
Expression<Func<CatalogItem, bool>> Filter = CatalogItem => true;
then as I go I add to it.
if(!string.IsNullOrEmpty(searchVal)) {Filter = Filter.Compose(CatalogItem => CatalogItem.Title.ToLower().Contains(searchVal), Expression.And);}
I may have different if conditions that cause me to add more and more to my filter then finally I want to apply that filter against my collection.
IQueryable<CatalogItem> FilteredList = Items.AsQueryable().Where(Filter);
In this case Items is a list of CatalogItems that was pulled from the database earlier. I am using IQueryable instead of just List<> cause I need to act on the results afterwards.
List<CatalogItem> Items;//fill Items list here;
I know this is in-line Linq as opposed to the structure you use but I really can't get into the other form of linq querying as this is so quick and dirty for me. If you have to use the other linq form these rules may still apply.

Delete records with multiple ids based on condition

Below is my class :
public partial class Ads
{
public int Id { get; set; }
public int RegionId { get; set; }
public string Name { get; set; }
public int Group { get; set; }
}
Records :
Id Name Group
1 abc 1
2 xyz 1
3 lmn 1
4 xxx 2
5 ppp 2
6 ttt 3
7 ggg 3
Now I want to remove all records/only that record with particular id of same group for some ids.
Code :
public void Delete(int[] ids,bool flag = false)
{
using (var context = new MyEntities())
{
context.Ads.RemoveRange(
context.Ads.Where(t => (flag ?
(context.Ads.Any(x => ids.Contains(x.Id) && x.Group == t.Group)) : false)));
context.SaveChanges();
}
}
What I am trying to do is something like below :
If flag is false with ids=3,5 then
I want to delete only records with Id=3,5
Else if flag is true with ids=3,5 then
I want to delete records with Id=3,5 but all other records too of the group to which ids=3,5 belong to.
Here id=3 belongs to group 1 so I want to delete all records of group1 i.e id=1,2 like wise ids=5 belongs to
group 2 so I want to delete all records of group 2 i.e id=4.
Expected output for this last case(flag=true) :
Id Name Group
6 ttt 3
7 ggg 3
But I think that I haven't done this is a proper way, and there is some source of improvement in the query.
Note : ids[] will always contains ids from different group and that too highest ids from different group.
How can I to improve my query for both the cases(flag=true and false)?
What about
var removeRecs=context.Ads.where(t => ids.contains(t.id))
if(flag)
removeRecs.AddRange(context.Ads.where(t=> removeRecs.Any(r =>t.groupId==r.Id)))
Ads.RemoveRange(removeRecs);
Do not make it too hard for your self, not everything must/can be done in the where statement of a query. Also a general rule of thumb in a loop try to factor out all the constant values and checks. So try this:
public static void Delete(int[] ids, bool flag = false)
{
using (var context = new MyEntities())
{
var query = context.Ads.AsQueryable();
query = flag
? query.Where(x => context.Ads
.Where(i => ids.Contains(i.Id))
.Select(i => i.Group)
.Contains(x.Group))
: query.Where(x => ids.Contains(x.Id));
context.Ads.RemoveRange(query);
context.SaveChanges();
}
}
public void Delete(int[] ids, bool flag = false)
{
using (var context = new MyEntities())
{
var items = context.Ads.Where(x => ids.Any(a => x.Id == a));
if (!flag)
{
//flag=false --> delete items with Id in ids[]
context.Ads.RemoveRange(items);
}
else
{
var groups = items.GroupBy(a => a.Group).Select(a => a.Key);
//flag=true --> delete all items in selected groups
context.Ads.RemoveRange(context.Ads.Where(x => groups.Any(a => x.Group == a)));
}
context.SaveChanges();
}
You should separate your tasks...
if (flag)
{
groupIds = db.Ads.Where(x => ids.Contains(x.Id)).Select(x => x.Group).ToList();
db.Ads.RemoveRange(db.Ads.Where(x => groupIds.Contains(x.Group)).ToList());
}
else
{
db.Ads.RemoveRange(db.Ads.Where(x => ids.Contains(x.Id)).ToList());
}
To me it looks like you have two different deletes here.
In first case you are only deleting the ads with given ID and this is pretty straight forward.
In second case you are deleting the ads with given ID and all other ads that contain the group of the recently deleted Ads. So in this case instead of deleting the ads with given Id first why not actualy get distinct groups for these ID-s and than just delete the groups.
EDIT
You can do it like this.
using (var context = new TestEntities())
{
if (!flag)
context.Ads.RemoveRange(context.Ads.Where(a => ids.Contains(a.Id)));
else
context.Ads.RemoveRange(context.Ads.Where(a => context.Ads.Where(g => ids.Contains(g.Id)).Select(x => x.Group).Distinct().Contains(a.Group)));
context.SaveChanges();
}
For the more complicated case I am trying to get distinct groups for given id-s. So for ID-s 3 and 5 I am selecting the groups and than I am doing distinct on the groups since it might happen that the id-s have the same group. Than I am fetching all the ads that have these groups. So for passed values of 3 and 5 I would get groups 1 and 2 which I would than use to get all the ads that have that group. That in turn would yield id-s 1, 2, 3, 4, and 5 which I would than delete.
EDIT 2
If the complexity of second Linq query bothers you than write a SQL query.
context.Database.ExecuteSqlCommand(
"DELETE Ads WHERE Group IN (SELECT Group FROM Ads WHERE Id IN(#p1, #p2))", new SqlParameter("#p1", ids[0]), new SqlParameter("#p2", ids[1]));
This should be extra performant rather than rely on EF which will delete it one by one.

Linq - complex query - list in a list

I have this class:
public class RecipeLine
{
public List<string> PossibleNames { get; set; }
public string Name { get; set; }
public int Index { get; set; }
}
I have a list of multiple RecipeLine objects. For example, one of them looks like this:
Name: apple
PossibleNames: {red delicious, yellow delicious, ... }
Index = 3
I also have a table in my db which is called tblFruit and has 2 columns: name and id. the id isn't the same as the index in the class.
What I want to do is this:
for the whole list of RecipeLine objects, find all the records in tblFruit whose name is in PossibleNames, and give me back the index of the class and the id in the table. So we have a list in a list (a list of RecipeLine objects who have a list of strings). How can I do this with Linq in c#?
I'm pretty sure there isn't going to be a LINQ statement that you can construct for this that will create a SQL query to get the data exactly how you want. Assuming tblFruit doesn't have too much data, pull down the whole table and process it in memory with something like...
var result = tblFruitList.Select((f) => new {Id = f.id, Index = recipeLineList.Where((r) => r.PossibleNames.Contains(f.name)).Select((r) => r.Index).FirstOrDefault()});
Keeping in mind that Index will be 0 if there isn't a recipeLine with the tblFruit's name in it's PossibleNames list.
A more readable method that doesn't one-line it into a nasty linq statement is...
Class ResultItem {
int Index {get;set;}
int Id {get;set;}
}
IEnumerable<ResultItem> GetRecipeFruitList(IEnumerable<FruitItem> tblFruitList, IEnumerable<RecipeLine> recipeLineList) {
var result = new List<ResultItem>();
foreach (FruitItem fruitItem in tblFruitList) {
var match = recipeLineList.FirstOrDefault((r) => r.PossibleNames.Contains(fruitItem.Name));
if (match != null) {
result.Add(new ResultItem() {Index = match.Index, Id = fruitItem.Id});
}
}
return result;
}
If tblFruit has a lot of data you can try and pull down only those items that have a name in the RecipeLine list's of PossibleName lists with something like...
var allNames = recipeLineList.SelectMany((r) => r.PossibleNames).Distinct();
var tblFruitList = DbContext.tblFruit.Where((f) => allNames.Contains(f.Name));
To get all the fruits within your table whose Name is in PossibleNames use the following:
var query = myData.Where(x => myRecipeLines.SelectMany(y => y.PossibleNames).Contains(x.Name));
I don't think you can do this in a single step.
I would first create a map of the possible names to indexes:
var possibleNameToIndexMap = recipes
.SelectMany(r => r.PossibleNames.Select(possibleName => new { Index = r.Index, PossbileName = possibleName }))
.ToDictionary(x => x.PossbileName, x => x.Index);
Then, I would retrieve the matching names from the table:
var matchingNamesFromTable = TblFruits
.Where(fruit => possibleNameToIndexMap.Keys.Contains(fruit.Name))
.Select(fruit => fruit.Name);
Then you can use the names retrieved from the tables as keys into your original map:
var result = matchingNamesFromTable
.Select(name => new { Name = name, Index = possibleNameToIndexMap[name]});
Not fancy, but it should be easy to read and maintain.

Categories