LINQ Group By Count for Many to Many - c#

I have 2 entities, with a 1 to many relationship, and I'm going to switch it to many to many but I need help with grouping and counts.
SearchString -> many JobResults
A SearchSting is used to find job results and job results are stored as a collection property of SearchString:
public class SearchString
{
public int SearchStringId { get; set; }
public string SearchStringName { get; set; }
public string query { get; set; }
public JobFunction JobFunction { get; set; }
public JobSeniority JobSeniority { get; set; }
public virtual ICollection<JobSearchResult> results { get; set; }
}
public class JobSearchResult
{
public int JobSearchResultId { get; set; }
public string jobtitle { get; set; }
public string company { get; set; }
public virtual SearchString SearchString { get; set; }
}
I get the top 5 JobFunctions of all job results as follows:
var top5jobfunctions = JobSearchResults.Where(a => (a.SearchString != null)).
GroupBy(s => new { s.SearchString.JobFunction.JobFunctionId, s.SearchString.JobFunction.JobFunctionName }).
Select(g => new { value = g.Key.JobFunctionId, displayname = g.Key.JobFunctionName, count = g.Count() }).
OrderByDescending(x => x.count).
Take(5).ToList();
I'm going to switch it to many to many as such:
public class SearchString
{
public int SearchStringId { get; set; }
public string SearchStringName { get; set; }
public string query { get; set; }
public JobFunction JobFunction { get; set; }
public JobSeniority JobSeniority { get; set; }
public virtual ICollection<JobSearchResult> results { get; set; }
}
public class JobSearchResult
{
public int JobSearchResultId { get; set; }
public string jobtitle { get; set; }
public string company { get; set; }
public virtual ICollection<SearchString> SearchStrings { get; set; }
}
How do I get my top 5 jobfunctions counts once I switch it to many to many?
Also, is the structure I chose the right approach? For example I wonder if having jobresults a child collection of SearchString was maybe not the best way to go and that perhaps I should just have SearchStrings be a collection property of JobResult.

For the modified model with many many relationship, consider the following modification to your original query:
var top5jobfunctions =
JobSearchResults.SelectMany(j => j.SearchString.Select(s => new {j,s}))
.Where(j => (j.s != null))
.GroupBy(j => new { j.s.JobFunction.JobFunctionId, j.s.JobFunction.JobFunctionName })
.Select(g => new { value = g.Key.JobFunctionId, displayname = g.Key.JobFunctionName, count = g.Count() })
.OrderByDescending(x => x.count)
.Take(5).ToList();
Explanation:
Now since JobSearchResult contains ICollection<SearchString>, it needs flattening to execute a similar query as earlier
SelectMany flattens the data and fills the results as an anonymous type, which contains a record for each SearchString
Henceforth similar logic as you have designed is followed
Model Correctness
I would not prefer, this kind of relationship, as it makes overall querying and data insertion unnecessarily complex
In my understanding a 1 to Many relationship would do as good a job in fetching all the relevant information, in this case you may consider just having ICollection<JobSearchResult> aggregated inside SearchString or vice versa relationship based on suitability, I am not sure what kind of use case does a circular many many relationship model solve.

Related

Entity Framework materializes collections inside IQueryable methods before their execution

On my previous work EF keeps query as IQueryable until I used some materialized methods, for example, ToList() or FirstOrDefault(), and I could create big, flexible and fast queries. But on my new work I noted, that sequences inside IQueryable methods have ICollection type and, of course, they have IEnumerable methods (not IQueriable). I can't understand what wrong and how I can change it. I haven't find solution in google. The version of EF is the same as on my previous work (6.1.3).
For Example, I have 2 entity classes and my own class:
public class Client // Entity
{
public int ID { get; set; }
public string FullName { get; set; }
public string Address { get; set; }
...
public virtual ICollection<Parcel> ParcelsSender { get; set; }
public virtual ICollection<Parcel> ParcelsReceiver { get; set; }
}
public class Parcel // Entity
{
public int ID { get; set; }
public string ParcelNumber { get; set; }
...
public int ClientSenderID { get; set; }
public int ClientReceiverID { get; set; }
public virtual Client ClientSender { get; set; }
public virtual Client ClientReceiver { get; set; }
}
public class ClientCustom // My class
{
public int ID { get; set; }
public string FullName { get; set; }
public bool IsInexperienced { get; set; }
}
and I created this EF query:
var clients = context.Clients
.OrderBy(x => x.FullName)
.Select(x => new ClientCustom()
{
ID = x.ID,
FullName = x.FullName,
IsInexperienced = x.ParcelsSender.Select(y => y.ID).FirstOrDefault() == 0
&& x.ParcelsReceiver.Select(y => y.ID).FirstOrDefault() == 0
})
.ToList();
In this case, the problem is that x.ParcelsSender and x.ParcelsReceiver in query are ICollection<Parcel> type; In turn, x.ParcelsSender.Select() and x.ParcelsReceiver.Select() methods returns IEnumerable<Parcel> instead IQueriable<Parcel>. As I know, that means, queries become very slowly with big amount of data.

Merging data from two tables into one view

I am having some trouble getting the data from two different tables into one view. If you know how to do this please let me know. This is what I'm working with:
I have four tables:
public class CoinAllocation
{
public int CoinAllocationID { get; set; }
public int? StoreID { get; set; }
public int? TimeFrameID { get; set; }
public virtual Store Store { get; set; }
public virtual TimeFrame TimeFrame { get; set; }
public virtual List<CoinAllocationItem> CoinAllocationItems { get; set; }
}
public class CoinAllocationItem
{
public int CoinAllocationItemID { get; set; }
public int? CoinID { get; set; }
public int? StoreID { get; set; }
public int? CoinAllocationID { get; set; }
public int QuantityAllocated { get; set; }
public virtual Coin Coin { get; set; }
}
public class CoinUsed
{
public int CoinUsedID { get; set; }
public int? TimeFrameID { get; set; }
public int? StoreID { get; set; }
public virtual Store Store { get; set; }
public virtual TimeFrame TimeFrame { get; set; }
public virtual List<CoinUsedItem> CoinUsedItems { get; set; }
}
public class CoinUsedItem
{
public int CoinUsedItemID { get; set; }
public int? CoinUsedID { get; set; }
public int? CoinID { get; set; }
public int? QuantityUsed { get; set; }
public virtual Coin Coin { get; set; }
public int? StoreID { get; set; }
}
Now, I need iterate through these tables to find coins that are from the same store and the same time frame. Then, I need to combine coins with the same ID, total their allocation amount, and then total the amount that they have used. Last, I need to get them into one view that is set up like this:
Coin Name | Amount Allocated | Amount Used | Remaining
silver coin 10 1 9
gold coin 15 5 10
and so on...
So, if there are two silver coins from the same store during the same time frame, they show up in the table in just one line, with the totals.
The problem I am having is getting the allocated from one table and getting the used from the other table.
Anyone out there who can help will be amazing.
Generally, you have to consider the following steps:
Filter your result by desired TimeFrame and Shop (LINQ Where)
Select the properties that you are interested in or that are needed for further computations (LINQ Select and SelectMany)
Group the results and compute sums (LINQ GroupBy)
Join different sub-results, select final properties (LINQ Join and GroupJoin)
There's always more than one way. I imagine that using GroupJoin at some point might be more efficient than what I currently came up with and if you start with Coin instead of separately handling CoinAllocation and CoinUsed, you might get a better structured code depending on the available navigation properties...
The following is what I came up with, which might or might not satisfy your needs - there are some uncertainties in your presented model and criteria.
// whatever you search for... this assumes you want coins for one store in one timeframe
int desiredStoreID = 0, desiredTimeFrameID = 0;
var coinUsedSelection = db.CoinUsed
.Where(x => x.StoreID == desiredStoreID && x.TimeFrameID == desiredTimeFrameID)
.SelectMany(x => x.CoinUsedItems)
.GroupBy(x => x.CoinID, x => x.QuantityUsed, (k, v) => new { CoinID = k, QuantityUsedSum = v.Sum() });
var coinAllocationSelection = db.CoinAllocations
.Where(x => x.StoreID == desiredStoreID && x.TimeFrameID == desiredTimeFrameID)
.SelectMany(x => x.CoinAllocationItems)
.GroupBy(x => new { x.CoinID, x.Coin.CoinName }, x => x.QuantityAllocated, (k, v) => new { k.CoinID, k.CoinName, QuantityAllocatedSum = v.Sum() });
var result = coinAllocationSelection.Join(coinUsedSelection, ca => ca.CoinID, cu => cu.CoinID, (ca, cu) => new
{
CoinName = ca.CoinName,
AmountAllocated = ca.QuantityAllocatedSum,
AmountUsed = cu.QuantityUsedSum,
Remaining = ca.QuantityAllocatedSum - cu.QuantityUsedSum
})
.ToList();

Error iterating the query results

I have a context called companyContext. There are Three tables Reports,Logs and Employees.I am given a case id and I need to get a list of all the employees who belong to a certain case (and a log if there is one, but i dont need to worry about that yet). So I made a query get all the employees where EmployeeID is equal to Employee.ID, where the Reports CaseID is eaual to case.id. However, its not reutning a list, its returning a Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable}]
Am I making the query correctly ? Thanks guys.
var employees = await context.Reports.Include(s => s.Employee)
.ThenInclude(e => e.ID)
.AsNoTracking()
.Where(r => r.CaseID == Case.Id);
Models
public class Log
{
public int ID { get; set; }
public string Input { get; set; }
public string Tag { get; set; }
public DateTime LogDate { get; set; }
public ICollection<Report> Reports { get; set; }
}
public class Employee
{
public int ID { get; set; }
public string Code { get; set; }
public string Name { get; set; }
public ICollection<Report> Reports { get; set; }
}
public class Report
{
public int ID { get; set; }
public string CaseID { get; set; }
public int EmployeeD { get; set; }
public int? LogID { get; set; }
public Employee Employee { get; set; }
public Log Log { get; set; }
}
SingleOrDefaultAsync
throws an exception if there is more than one element in the sequence.
So you should not be using SingleOrDefaultAsync if you expect multiple records.
If I'm interpreting your question properly and you want employees themselves, you could start with them and then narrow via the Employee class's .Reports navigation property.
var employees = await context.Employees.AsNoTracking()
.Where(e => e.Reports.Any(r => r.CaseID == case.Id))
.ToListAsync();
So entity is actually like using a real database. I had to do a where and select clause, but also a toList to convert it into something readable.
var employees = await context.Reports.Where(r => r.CaseID == Case.Id);
.Select(r => r.Employee)
.ToList();

Get data from nested table using entity framework

First of all this is my first question in the forum so please excuse me for any writing mistake.
I have 4 tables
attaching the table diagram
What I want is to get list of attraction name joining 'tblattraction' with 'tblattractionmaster' and count of the exact attraction for each place from 'tblattractions' using 'locationid' , I am using entity framework but don't know how to do that,
Disclaimer:
Each location can consist Multiple Places
Each Place can consist Multiple Attractions
What I have tried
return context.tblLocationMasters.Select(t => new details()
{
locationid = t.LocationId,
locationname = t.LocationName,
attractions =t.tblPlaces.SelectMany(a => a.tblAttractions).Select(b => new attractions(){
AttractionName=b.tblAttractionMaster.attractionname//(Not working),
TotalAttractions=0//???
}).ToList()
}).ToList();
I recreated your model (slightly different) using Code First. I came up with the following structure:
public class Location
{
public int LocationId { get; set; }
public string LocationName { get; set; }
public ICollection<Place> Places { get; set; }
}
public class Place
{
public int PlaceId { get; set; }
public string PlaceName { get; set; }
public int LocationId { get; set; }
public Location Location { get; set; }
public ICollection<AttractionPlace> Attractions { get; set; }
}
public class Attraction
{
public int AttractionId { get; set; }
public string AttractionName { get; set; }
}
public class AttractionPlace
{
public int AttractionPlaceId { get; set; }
public int PlaceId { get; set; }
public Place Place { get; set; }
public int AttractionId { get; set; }
public Attraction Attraction { get; set; }
}
Then, I could get the results in the way you needed with the following query:
var query = (from loc in db.Locations
join pla in db.Places.Include(x => x.Attractions) on loc.LocationId equals pla.LocationId
let count = pla.Attractions.Count()
select new
{
loc.LocationId,
loc.LocationName,
Attractions = pla.Attractions.Select(z => new
{
pla.PlaceName,
z.AttractionId,
z.Attraction.AttractionName
}),
AttractionsByPlaceCount = count
});
The query above returns data in this format
Just a side note though: I didn't went further to see the performance of this query. The SQL generated by Linq wasn't that bad, but you should consider analyzing it before actually using it in production.

How can I user LINQ to get a IList<string> from a multi-table join?

I have a SQL Server 2012 database and I am using Entity Framework 6.1 to access data.
I have the following entities that are joined to each other with Primary Key and Foreign Key relationships. What I would like to do is to get a simple collection of values of the Quid parameter in the Questions table where the taskId matches a value that is chosen outside of this code.
Task contains Objective contains ObjectiveDetail contains SubTopic contains Problems contations
Questions
I created the following LINQ statement. It seems not to work and I need some advice on what I am doing wrong and how the statement could be made to get what I need. In particular I am not sure about the way I do the join with all the many .Selects and also the select to give me the output.
var quids = db.Tasks
.Include(e => e.Objectives
.Select(o => o.ObjectiveDetails
.Select(od => od.SubTopics
.Select(s => s.Problems
.Select(p => p.Questions)))))
.Where(t => t.TaskId == taskId)
.Select(e => e.Objectives
.Select(o => o.ObjectiveDetails
.Select(od => od.SubTopics
.Select(s => s.Problems
.Select(p => p.Questions
.Select(q => q.QuestionUId))))))
.ToList();
However this gives me a very confusing output and certainly not the simple IList of Quids
that I would like. Here are my classes for reference. I removed additional fields and hopefully just left the important ones.
public class Task
{
public Task()
{
this.Objectives = new HashSet<Objective>();
}
public int TaskId { get; set; }
public virtual ICollection<Objective> Objectives { get; set; }
}
public class Objective
{
public Objective()
{
this.ObjectiveDetails = new HashSet<ObjectiveDetail>();
}
public int ObjectiveId { get; set; }
public int TaskId { get; set; }
public virtual Task Task { get; set; }
public virtual ICollection<ObjectiveDetail> ObjectiveDetails { get; set; }
}
public partial class ObjectiveDetail
{
public ObjectiveDetail()
{
this.SubTopics = new HashSet<SubTopic>();
}
public int ObjectiveDetailId { get; set; }
public int ObjectiveId { get; set; }
public virtual Objective Objective { get; set; }
public virtual ICollection<SubTopic> SubTopics { get; set; }
}
public class SubTopic
{
public SubTopic()
{
this.Problems = new HashSet<Problem>();
}
public int SubTopicId { get; set; }
public int Number { get; set; }
public int TopicId { get; set; }
public virtual ICollection<ObjectiveDetail> ObjectiveDetails { get; set; }
public virtual ICollection<Problem> Problems { get; set; }
}
public class Problem
{
public Problem()
{
this.Questions = new HashSet<Question>();
}
public int ProblemId { get; set; }
public int SubTopicId { get; set; }
public virtual SubTopic SubTopic { get; set; }
public virtual ICollection<Question> Questions { get; set; }
}
public class Question
{
public int QuestionId { get; set; }
public string QuestionUId { get; set; }
public virtual Problem Problem { get; set; }
}
Please note the many-many relationship between SubTopic and ObjectiveDetail. This I think makes it more difficult.
From what I understand of your need, you can simply start from the Question DbSet instead of the task:
var uids = db.Questions.Where(q => q.Problem.SubTopic
.ObjectiveDetails
.Any(od => od.Objective.TaskId == taskId))
.Select(q => q.QuestionUId)
.ToList();
This will take all Question which has at least one objective with the TaskId equal to taskId then project the QuestionUid and put it in a list.
Each of your outer Select calls produces a sequence for each item. This gives you nested sequences and probably a confusing ToString debug output.
What you probably meant was to use SelectMany to flatten the nested sequences.
The Include in your query cannot possibly help because you are just returning a list of strings. There is nothing to include in a string. Better remove it. Who knows what code EF generates from it.

Categories