Linq conditional group by count - linq

Consider having a workshop table with the following records:
id
name
1
Epic Car Repairs
2
Car Care Workshop - Alpha
3
Car Care Workshop - Bravo
4
Car Care Workshop - Charlie
5
Bosch Car Service Centre
Assume all the Car Care Workshop's with id's 2, 3, 4 are part of a franchise. If I have a list of quotes submitted by all of these workshops (assume each of the 5 workshops submitted 1 quote) and assume I have a list of id's for each workshop in the franchise, how do I go about counting the quotes in linq so that all the quotes of the the franchisee workshops count as only 1?
quotes.Count() gives a value of 5
quotes.Count('somehow identify distinct franchises') should give a value of 3

First you need to decide on a way to uniquely identify distinct values, for example name up to the first dash (-). Then you can count distinct values based on this property.
Perhaps something like this;
private class Workshop
{
public Workshop(int id, string name)
{
Id = id;
Name = name;
}
public int Id { get; }
public string Name { get; }
public string UniqueName => Name.Split('-').First().Trim();
}
I like to use a helper extension method so that I can get distinct values by a specific property / method.
public static IEnumerable<T> DistinctBy<T, T2>(this ICollection<T> collection, Func<T, T2> selector)
{
return collection.GroupBy(selector).Select(x => x.First());
}
Your scenario;
[Test]
public void DistinctBy_works()
{
ICollection<Workshop> collection = new List<Workshop>();
collection.Add(new Workshop(1, "Epic Car Repairs"));
collection.Add(new Workshop(2, "Car Care Workshop - Alpha"));
collection.Add(new Workshop(3, "Car Care Workshop - Bravo"));
collection.Add(new Workshop(4, "Car Care Workshop - Charlie"));
collection.Add(new Workshop(5, "Bosch Car Service Centre"));
var result = collection.DistinctBy(x => x.UniqueName).ToList();
result.Count.Should().Be(3);
result[0].Id.Should().Be(1);
result[1].Id.Should().Be(2);
result[2].Id.Should().Be(5);
}

Related

EF Core 3.1: Group by Sum

Let's suppose I have a list of player scores that looks somewhat like this:
public class ScoreEntry
{
public int PlayerId {get; set;}
public int Score {get; set;}
public string SomeOtherValue {get;set;}
}
And there are multiple entries for a single player, so I want to group those score entries and sum up their score values, something like this:
var query =
from entry in scores
group entry by entry.PlayerId into scoreEntryGroup
select new ScoreEntry()
{
PlayerId = scoreEntryGroup.Key,
Amount = scoreEntryGroup.Sum(g => g.Score),
SomeOtherValue = scoreEntryGroup.FirstOrDefault().SomeOtherValue
};
It throws an exception and I know that the problem is that I can't use SingleOrDefault() and many other similar methods in a group in EF Core 3.1
Could you help me find another way to achieve this? Any help would be highly appreciated!
Try the following query. It uses CROSS APPLY for joining to aggregation result.
var aggregations =
from entry in scores
group entry by entry.PlayerId into scoreEntryGroup
select new
{
PlayerId = scoreEntryGroup.Key,
Amount = scoreEntryGroup.Sum(g => g.Score)
};
var query =
from a in aggregations
from entry in scores
.Where(entry => entry.PlayerId == a.PlayerId)
.Take(1)
select new ScoreEntry()
{
PlayerId = entry.PlayerId,
SomeOtherValue = entry.SomeOtherValue,
Amount = a.Amount
};

Query sub collection - majority mutual ids that contains in list

Consider the following list:
List<long> listOfIDs = new List<long> { 1, 2, 3, 4 };
[(Product) 1 Pineapple - (Supplier) Fruit Inc / Marketplace Inc]>
[2 Strawberry - Fruit Inc]>
[3 Coke - Super Drinks Inc / Marketplace Inc]>
[4 Orange Juice - Super Drinks Inc]
db.Table.Where(a => a.SubTable.All(b => listOfIds.Contains(b.SubTableId)))
While I've selected Products 1 and 2, I should get only Fruit Inc as a Supplier. When I include Coke into my list, I don't want to see any supplier anymore because there is no Supplier that represents these 3 products at the same time.
Expected Outputs Scenarios:
Selected Products: 1, 2
Expected Result: Fruit Inc
1, 3
Marketplace Inc
1, 2, 3
Empty.
1, 3, 4
Empty.
3, 4
Super Drinks Inc
I think I finally got what's the issue.
So you have a two entities with many-to-many relationship like this:
public class Product
{
public long Id { get; set; }
public string Name { get; set; }
public ICollection<Supplier> Suppliers { get; set; }
}
public class Supplier
{
public long Id { get; set; }
public string Name { get; set; }
public ICollection<Product> Products { get; set; }
}
and given a list with product Ids, you want to get the list of suppliers that provide all the products from the list.
There are at least two ways you could do that:
List<long> productIds = ...;
(A) Similar to your attempt, but switching the roles of All andContains:
var suppliers = db.Suppliers
.Where(s => productIds.All(id => s.Products.Any(p => p.Id == id)))
.ToList();
or if you prefer Contains (both generate one and the same SQL):
var suppliers = db.Suppliers
.Where(s => productIds.All(id => s.Products.Select(p => p.Id).Contains(id)))
.ToList();
(B) Counting the matches:
var suppliers = db.Suppliers
.Where(s => s.Products.Count(p => productIds.Contains(p.Id)) == productIds.Count)
.ToList();
I can't say which one is better performance wise, but all they produce the desired result and are supported by LINQ to Entities.
Your problems depends on the fact that a given supplier can have more products related to it then those contained in your filter list.
You should resolve this issue simply exchanging the container of data source against which to test your condition in your linq query:
db.Table.Where(a => listOfIds.All(b => a.SubTable.Contains(b)))
You can also read this query in this way:
get all the suppliers (a records) for which is true that all
products (b elements) contained in listOfIds must be also
contained in products list related to each supplier (a.SubTable).

count based on lookup in LINQ

I have a table (or entity) named Cases. There is another table CaseStatus_Lookup and the primary key of this table is a foreign key in the Cases table.
What I want to do is: For every status type I want the number of count of cases. For e.g. if status = in progress , I want to know how many cases are in that status.
one other thing: I also want to filter the Cases based on UserID.
I tried several ways in LINQ but could not get vary far. I was wondering if someone could help.
try Linq .GroupBy
am assuming your entity structure
suppose your Case Entity is like
public class Case
{
public int Id{get;set;}
public int CaseStatusId{get;set;}
public int UserId{get;set;}
//navigational fields
public virtual CaseStatus CaseStatus {get;set;}
}
and suppose your CaseStatus entity is like:
public class CaseStatus
{
public int Id{get;set;}
public string Name{get;set;}
//navigational fields..
public virtual ICollection<Case> Cases{get;set;}
}
then you can do this:
using (myDbContext db = new myDbContext())
{
var query = db.Cases.GroupBy(case => case.CaseStatus.Name)
.Select(group =>
new {
Name = group.Key,
Cases= group.OrderBy(x => x.Id),
Count= group.Count()
}
).ToList();
//query will give you count of cases grouped by CaseStatus.
}
similarly you can further filter your result based on userId.
Start to explore about Linq .GroupBy
You need a function that returns the sum and takes the status as parameter :- something like below.
MyCaseStatusEnum caseStatus; //Pass your required status
int caseCount = myCases
.Where(r => r.Status == caseStatus)
.GroupBy(p => p.Status)
.Select(q => q.Count()).FirstOrDefault<int>();

Linq to Entities, Take(3) from Joined table

I am trying to populate a ViewModel in an MVC app with data from a parent table joined with a child table. The only data I want from the child table is a comma diliminated string from the Nomenclature field of the top three records and put them into a string field in the ViewModel. Here is what I have tried without success:
public IEnumerable<ReqHeaderVM> GetOpenReqs(string siteCode)
{
var openReqs = from h in context.ReqHeaders
join l in context.ReqLineItems on h.ID equals l.ReqID into reqLineItems
select new ReqHeaderVM
{
ReqID = h.ID,
ShopCode = h.ShopCode
Nomenclatures = reqLineItems.Select(x => x.Nomenclature).Take(3) // This doesn't work
};
return (openReqs.ToList());
}
Here is the ViewMdel:
public class ReqHeaderVM
{
[Editable(false)]
public string ReqID { get; set; }
public string ShopCode { get; set; }
public string Nomenclatures {get; set;}
}
Assuming that you have proper relationship (foreign key) between ReqHeaders and ReqLineItems, this should give you what you are looking for...
public IEnumerable<ReqHeaderVM> GetOpenReqs(string siteCode)
{
var openReqs = from h in context.ReqHeaders
select new
{
ReqID = h.ID,
ShopCode = h.ShopCode
Nomenclatures = h.ReqLineItems
.OrderBy(x => x.SomeColumn)
.Select(x => x.Nomenclature)
.Take(3)
};
var openReqsTran = from oreq in openReqs.AsEnumerable()
select new ReqHeaderVM
{
oreq.ReqID,
oreq.ShopCode,
Nomenclatures = string.Join(", ", oreq.Nomenclatures)
};
return (openReqsTran);
}
Note that Nomenclatures is a list of type of Nomenclature.
Yes, the join creates a single Cartesian result set. (think tabular data) what you are attempting to do. To get the results you want you have a few choices.
use lazy loading and iterate over each header querying the line items individually.
pro - simple queries
con - select n+1
query all headers and all line items, but build view model with only the top 3
pro - single query
con - large Cartesian result set query too much data
query all headers and all associated lines individuals
pro - 2 smaller, simpler queries
con - query too many line details.
query all headers and top 3 lines per header in 2 queries
pro - get only the information you require
con - complex query for top 3 lines per header.

Getting all tags from my different records

I use EntityFramework on my ASP.NET MVC project.
Let's say I have the entity below:
public class Project
{
public int ProjectID { get; set; }
public string Description { get; set; }
public string Tags { get; set; }
}
Lets say I have the following data in my DB:
ProjectID: 1
Description: "My first element"
Tags: "one, three, five, seven"
ProjectID: 2
Description: "My second element"
Tags: "one, two, three, six"
ProjectID: 3
Description: "My third element"
Tags: "two, three, four"
I would like to collect all tags from all my records. So: "one, two, three, four, five, six, seven"
How can I do? This may seems a stupid question but I don't know how to proceed.
Thanks.
You need to use string.Split() to dig out each tag in your list.
HashSet<string> allTags = new HashSet<string>();
foreach(Project project in context.Projects)
{
string tagsList = project.Tags;
string[] separateTags = tagsList.Split(", ", StringSplitOptions.RemoveEmptyEntries);
foreach(string separateTag in separateTags)
{
allTags.Add(separateTag);
}
}
then allTags will contain all your tags. If you want to put them in one big string again, use string.Join.
After splitting the strings you can use SelectMany() to concatenate the collections, and then use Distinct() to remove duplicates.
var tags = context.Projects
.SelectMany(p => p.Tags.Split(", ", StringSplitOptions.RemoveEmptyEntries))
.Distinct();
Here is the query syntax version, which gets transated to a SelectMany() statement behind the scenes:
var tags = (from p in project
from tag in p.Tags.Split(", ", StringSplitOptions.RemoveEmptyEntries)
select tag).Distinct();
Unfortunately, Split() won't translate to SQL, so you have to do that in memory. I'd recommend the following:
var tags = context.Projects
// pull only the tags string into memory
.Select(p => p.Tags)
// operate in memory from this point on
.AsEnumerable()
// remove empty entries so you don't get "" interpreted as one empty tag
.SelectMany(tags => tags.Split(",", StringSplitOptions.RemoveEmptyEntries))
// assuming you don't want leading or trailing whitespace
.Select(tag => tag.Trim())
.ToList();

Resources