LINQ Join Group By Select New - linq

I have the following entities:
User is:
public String Id { get; set; }
public String FirstName { get; set; }
public String LastName { get; set; }
Assessment is:
public int Id { get; set; }
public int SymptomId { get; set; }
public string UserId { get; set; }
public DateTime CreatedDate { get; set; }
A User can have 0, 1 or more Assessments.
I have written the following LINQ:
// Get Paged Users Recent Assessment List:
var _usersWithRecentAssessment =
from U in _context.Users
join A in _context.Assessments on U.Id equals A.UserId
group A by A.UserId into uaGroup
select uaGroup.OrderByDescending(a => a.CreatedDate).FirstOrDefault();
_usersWithRecentAssessment = _usersWithRecentAssessment.OrderByDescending(ua => ua.CreatedDate);
which returns the most recent symptom assessment for all Users that have completed an Assessment (and orders the assessment list in descending order of Assessment CreatedDate) as follows:
[
{
"id": 1052,
"symptomId": 44,
"userId": "b978d113-7da7-4b7f-a121-9dd71e158dd4",
"createdDate": "2019-11-16T12:50:05.2175621"
},
{
"id": 1051,
"symptomId": 44,
"userId": "5230f4b7-bf2a-46b0-88a0-6f13fa5caa91",
"createdDate": "2019-11-03T14:46:21.6598763"
}
]
I would like to return the following AssessmentDTO
where AssessmentDTO is:
public int Id { get; set; }
public int SymptomId { get; set; }
public string UserId { get; set; }
public string UserFirstName { get; set; }
public string UserLastName { get; set; }
public DateTime CreatedDate { get; set; }
which contains the additional attributes UserId, UserFirstName, and UserLastName from the User entity.
I have tried unsuccessfully to add 'select new AssessmentDTO() { }' at the end of the LINQ.
Can someone please help me?

A User has zero or more Assessments, every Assessment belongs to exactly one User. This is a standard one-to-many relation with a foreign key.
For your problem, you can use one of the overload of Queryable.GroupBy. First get all Users with their assessments and as a result keep only the most recent assessment:
var UsersWithMostRecentAssessment = dbContext.Users
.GroupBy(dbContext.Assessments, // GroupJoin Users and Assessments
user => user.Id, // From every User take the Id
assessment => assessment.UserId, // From every Assessment take the UserId
// ResultSelector: take each User with its zero or more Assessments to make one new:
(user, assessmentsOfThisUser) => new
{
User = user,
MostRecentAssessment = assessmentsOfThisUser
.OrderByDescending(assessment => assessment.CreatedDate)
.FirstOrDefault(),
// might be null if this User has not assessments at all
})
// Now get the User with its MostRecentAssessment (or null) to make one new:
.Select(userAssessMent => new
{
// Values from the most recent assessment:
Id = userAssessment.MostRecentAssessment.Id,
SymptomId = userAssessment.MostRecentAssessment.SymptomId,
...
// Values from the User:
UserId = userAssessment.User.Id,
FirstName = userAssessment.User.FirstName,
LastName = userAssessment.User.LastName,
...
})
Note: this will go wrong if there are users without assessments, because they won't have a most recent assessment. You can omit these users before the final Select:
.Where(userAssessment => userAssessment.MostRecentAssessment != null)
Or if you want these Users in your endresult:
// Values from the most recent assessment or default if null
Id = userAssessment.MostRecentAssessment.Id ?? 0,
SymptomId = userAssessment.MostRecentAssessment.SymptomId ?? 0,

Related

product sale qty sum and name in linq group query

I have a product sales data and want to show the summary of sale grouped by product id.
Summary result should show product name and total sales. How can I select a field along with groupby result and that field is not the key field.
public partial class SaleOrderDetail
{
public int Id { get; set; }
public int ProductId { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
public decimal LineTotal { get; set; }
}
var query = from saleorder in _dbContext.SaleOrderDetail
group saleorder by saleorder.ProductId into salesummary
select new
{
productid = salesummary.Key,
prdouctname = salesummary.First().ProductName,
totalqty = salesummary.Sum(s => s.Quantity)
};
I got the error invalidoperationException because of First() for product name.
You have to include ProductName in grouping Key.
var query =
from saleorder in _dbContext.SaleOrderDetail
group saleorder by new {saleorder.ProductId, saleorder.ProductName} into salesummary
select new
{
productid = salesummary.Key.ProductId,
prdouctname = salesummary.Key.ProductName,
totalqty = salesummary.Sum(s => s.Quantity)
};
Making SaleOrderDetail as AsEnumerable() did the trick. For SQL Expression it will work if make it as AsEnumerable() or .ToList<> etc.
var query = from saleorder in _dbContext.SaleOrderDetail.AsEnumerable()
group saleorder by saleorder.ProductId into salesummary
select new
{
productid = salesummary.Key,
prdouctname = salesummary.First().ProductName,
totalqty = salesummary.Sum(s => s.Quantity)
};

EF Core - many queries sent to database for subquery

Using EF Core 2.2.2, I have a table in my database which is used to store notes for many other tables. In other words, it's sortof like a detail table in a master-detail relationship, but with multiple master tables. Consider this simplified EF Model:
public class Person
{
public Guid PersonID { get; set; }
public string Name { set; set; }
}
public class InvoiceItem
{
public Guid InvoiceItemID { get; set; }
public Guid InvoiceID { get; set; }
public string Description { get; set; }
}
public class Invoice
{
public Guid InvoiceID { get; set; }
public int InvoiceNumber { get; set; }
public List<Item> Items { get; set; }
}
public class Notes
{
public Guid NoteID { get; set; }
public Guid NoteParentID { get; set; }
public DateTime NoteDate { get; set; }
public string Note { get; set; }
}
In this case, Notes can store Person notes or Invoice notes (or InvoiceItem notes, though let's just say that the UI doesn't support that).
I have query methods set up like this:
public IQueryable<PersonDTO> GetPersonQuery()
{
return from p in Context.People
select new PersonDTO
{
PersonID = p.PersonID,
Name = p.Name
};
}
public List<PersonDTO> GetPeople()
{
return (from p in GetPersonQuery()
return p).ToList();
}
public IQueryable<InvoiceDTO> GetInvoiceQuery()
{
return from p in Context.Invoices
select new InvoiceDTO
{
InvoiceID = p.InvoiceID,
InvoiceNumber = p.InvoiceNumber
};
}
public List<InvoiceDTO> GetInvoices()
{
return (from i in GetInvoiceQuery()
return i).ToList();
}
These all work as expected. Now, let's say I add InvoiceItems to the Invoice query, like this:
public IQueryable<InvoiceDTO> GetInvoiceQuery()
{
return from p in Context.Invoices
select new InvoiceDTO
{
InvoiceID = p.InvoiceID,
InvoiceNumber = p.InvoiceNumber,
Items = (from ii in p.Items
select new ItemDTO
{
ItemID = ii.ItemID,
Description = ii.Description
}).ToList()
};
}
That also works great, and issues just a couple queries. However, the following:
public IQueryable<InvoiceDTO> GetInvoiceQuery()
{
return from p in Context.Invoices
select new InvoiceDTO
{
InvoiceID = p.InvoiceID,
InvoiceNumber = p.InvoiceNumber,
Items = (from ii in p.Items
select new ItemDTO
{
ItemID = ii.ItemID,
Description = ii.Description
}).ToList(),
Notes = (from n in Context.Notes
where i.InvoiceID = n.NoteParentID
select new NoteDTO
{
NoteID = n.NoteID,
Note = n.Note
}).ToList(),
};
}
sends a separate query to the Note table for each Invoice row in the Invoice table. So, if there are 1,000 invoices in the Invoice table, this is sending something like 1,001 queries to the database.
It appears that the Items subquery does not have the same issue because there is an explicit relationship between Invoices and Items, whereas there isn't a specific relationship between Invoices and Notes (because not all notes are related to invoices).
Is there a way to rewrite that final query, such that it will not send a separate note query for every invoice in the table?
The problem is indeed the correlated subquery versus collection navigation property. EF Core query translator still has issues processing such subqueries, which are in fact logical collection navigation properties and should have been processed in a similar fashion.
Interestingly, simulating collection navigation property with intermediate projection (let operator in LINQ query syntax) seems to fix the issue:
var query =
from i in Context.Invoices
let i_Notes = Context.Notes.Where(n => i.InvoiceID == n.NoteParentID) // <--
select new InvoiceDTO
{
InvoiceID = i.InvoiceID,
InvoiceNumber = i.InvoiceNumber,
Items = (from ii in i.Items
select new ItemDTO
{
ItemID = ii.ItemID,
Description = ii.Description
}).ToList(),
Notes = (from n in i_Notes // <--
select new NoteDTO
{
NoteID = n.NoteID,
Note = n.Note
}).ToList(),
};

How and where to use AddRange() method

I want to display related data from second table with each value in first table
i have tried this query
public ActionResult Index()
{
List<EmployeeAtt> empWithDate = new List<EmployeeAtt>();
var employeelist = _context.TblEmployee.ToList();
foreach (var employee in employeelist)
{
var employeeAtt = _context.AttendanceTable
.GroupBy(a => a.DateAndTime.Date)
.Select(g => new EmployeeAtt
{
Date = g.Key,
Emp_name = employee.EmployeeName,
InTime = g.Any(e => e.ScanType == "I") ? g.Where(e =>
e.ScanType == "I").Min(e =>
e.DateAndTime.ToShortTimeString())
.ToString() : "Absent",
OutTime = g.Any(e => e.ScanType == "O") ? g.Where(e =>
e.ScanType == "O").Max(e =>
e.DateAndTime.ToShortTimeString())
.ToString() : "Absent"
});
empWithDate.AddRange(employeeAtt);
}
return View(empWithDate);
}
Here is my attendance Table
AttendanceTable
Results
I want to display the shortest time with "I" Column value against each employee and last time with "O" Column value as out time. I think i am not using AddRange() at proper place. Where it should go then?
public partial class TblEmployee
{
public TblEmployee()
{
AttendanceTable = new HashSet<AttendanceTable>();
}
public int EmpId { get; set; }
public string EmployeeName { get; set; }
public virtual ICollection<AttendanceTable> AttendanceTable { get; set; }
}
public partial class AttendanceTable
{
public int Id { get; set; }
public int AttendanceId { get; set; }
public int EmployeeId { get; set; }
public string ScanType { get; set; }
public DateTime DateAndTime { get; set; }
public virtual TblEmployee Employee { get; set; }
}
The actual problem is not related to AddRange(), you need a where clause before GroupBy() to limit attendances (before grouping) to only records related to that specific employee, e.g.
_context.AttendanceTable
.Where(a => a.Employee == employee.EmployeeName)
.GroupBy(a => a.DateAndTime.Date)
...
Depended on your model, it is better to use some kind of ID instead of EmployeeName for comparison if possible.
Also you can use SelectMany() instead of for loop and AddRange() to combine the results into a single list. like this:
List<EmployeeAtt> empWithDate = _context.TblEmployee.ToList()
.SelectMany(employee =>
_context.AttendanceTable
.Where(a => a.Employee == employee.EmployeeName)
.GroupBy(a => a.DateAndTime.Date)
.Select(g => new EmployeeAtt
{
...
})
);
...

Selecting single element from each group that cointains maximum DateTime value

model :
public class ReferenceParameterHistory
{
[Key]
public int IDReferenceParameterHistory { get; set; }
public double Value { get; set; }
public string Value_S { get; set; }
public DateTimeOffset CreatedAt { get; set; }
[Required]
public bool IsStable { get; set; }
public int? IDReference { get; set; }
public Reference Reference { get; set; }
[Required]
public int IDParameterTemplate { get; set; }
public ParameterTemplate ParameterTemplate { get; set; }
}
My code in ASP.NET core controller :
[HttpGet]
public async Task<IActionResult> GetReferenceParameterHistory(int? IDparameterTemplate,
int? IDreference,
DateTimeOffset? startDate,
DateTimeOffset? endDate,
bool latestOnly)
{
try
{
IQueryable<ReferenceParameterHistory> query = _context.ReferenceParameterHistory.OrderByDescending(rph => rph.IDReferenceParameterHistory);
if (IDparameterTemplate != null && IDparameterTemplate > 0)
query = query.Where(rph => rph.IDParameterTemplate == IDparameterTemplate);
if (IDreference != null && IDreference > 0)
query = query.Where(rph => rph.IDReference == IDreference);
if (startDate != null)
query = query.Where(rph=> rph.CreatedAt >= startDate);
if (endDate != null)
query = query.Where(rph => rph.CreatedAt <= endDate);
if (latestOnly)
{
// I tried this but it doesnt compile and I don't have idea how to solve this ....
//query = (from rph in query
// group rph by rph.IDParameterTemplate
// into groups
// where groups.Max(rph => rph.CreatedAt)
// select groups.Key);
}
var referenceParameterHistory = await query.AsNoTracking().ToListAsync();
if (referenceParameterHistory.Any())
return new ObjectResult(referenceParameterHistory);
return new NotFoundResult();
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return new StatusCodeResult(500);
}
}
I have a database table based on that ReferenceParameterHistory model class. I want to group records exctracted from that table by IDParameterTemplate and from each group I need to extract records that have the highest value in CreatedAt column (latest records). So each group contains many recods but I need to get only these with max value in CreatedAt column. The result should be IEnumerable of ReferenceParameterHistory since I store that query in an IQueryable variable and then send it to SQL Server to process the query. Commented code in my example is just what I tried but I don't know how to do that.
How can I solve that problem ?
You reused the variable rph inside the lambda.
How to select only the records with the highest date in LINQ
query = (from rph in query
group rph by rph.IDParameterTemplate into g
select g.OrderByDescending(t=>t.CreatedAt).FirstOrDefault());

Group records by id

I am getting records like this returned from a stored procedure:
Id FullName Review Rating
---------------------------------------------
1 john sk 4.5
1 john hhh 3.5
1 john jhj 1.5
2 rig www 3.5
2 rig eee 1.5
This is my query which return records like above:
var empDetails = context.Database.SqlQuery<SearchWorkerDetail>("exec SearchWorkerDetail #param1", new SqlParameter("param1", searchKeyword) )
.Select( d => new
{
id = d.Id,
FullName = d.FullName,
Email = d.Email,
ServiceDescription = d.ServiceDescription,
Skills = d.Skills,
Name = d.Name,
r = d.Review,
averagerating = d.Rating
}).ToList();
Now I want to group the records by Id and want to select data.
Expected output:
SearchWorkerDetail=
[0]:{
Id:1
FullName:John
Email:john#yahoo.com
ReviewList:
{
[0]: Review=sk
rating=4.5
[1]: Review=hhh
rating=3.5
[2]: Review=jhj
rating=1.5
}
[1]:{
Id:2
FullName:rig
Email:rig#yahoo.com
ReviewList:
{
[0]: Review=www
rating=3.5
[1]: Review=eee
rating=1.5
}
}
For corresponding id I want list of reviews (1 to many relation between worker and review)
My class structure:
[DataContract]
public class SearchWorkerDetail
{
[DataMember]
public int Id { get; set; }
[DataMember]
public string FullName { get; set; }
[DataMember]
public string Email { get; set; }
[DataMember]
public string ServiceDescription { get; set; }
[DataMember]
public string Skills { get; set; }
[DataMember]
public string Name { get; set; }
[DataMember]
public string Review { get; set; }
[DataMember]
public Nullable<decimal> Rating { get; set; }
[DataMember]
public List<ReviewModel> RatingList { get; set; }
}
My query is like this:
var data = empDetails.GroupBy(m=>m.id)
.Select(g => new SearchWorkerDetail
{
id =g.Key,
FullName = g.FullName,
Reviewlist =
}
).ToList();
Here it is not allowing me to select FullName and other (eg: Email, skillDescription, Skills etc)
How do I do this?
There is no FullName property on g because it's a group not a single object. However you can get the FullName of first record in the group then create a ReviewList for each record in the group by using another Select:
empDetails.GroupBy(m => m.id)
.Select(g => new SearchWorkerDetail
{
id = g.Key,
FullName = g.First().FullName,
Reviewlist= g.Select(x => new ReviewModel
{
Review = x.Review,
rating = x.Rating
}.ToList()
}).ToList();
Or you can also group by based on FullName:
empDetails.GroupBy(m => new { m.id, m.FullName })
.Select(g => new SearchWorkerDetail
{
id = g.Key.id,
FullName = g.Key.FullName,
/* rest is the same */
}).ToList();

Resources