EF Core summary of nested fields - c#

I have a nested object similar to the diagram below:
Account
├─ Id
├─ Type
├─ Name
└─ Customers[]
├─ Id
├─ Name
└─ Reports[]
├─ Id
├─ Status
└─ Measurements[]
├─ Id
├─ ValueA
└─ ValueB
One of the requirements is to create a summary of each account, so I created a separate entity and a specification to get the data I need:
public class AccountSummary : EntityBase<int> {
public string AccountName { get; set; }
public int CustomerCount {get; set; }
public int TotalApprovedValueA { get;set; }
public int TotalApprovedValueB { get;set; }
}
public sealed class AccountSummarySpec : Specification<Account, AccountSummary> {
public AccountSummarySpec(AccountType type) {
this.Query.Select(a => new AccountTable
{
Id = a.Id,
AccountName = a.Name,
CustomerCount = a.Customers.Count(),
TotalApprovedValueA = a.Customers
.SelectMany(s => s.Reports.Where(r => r.Status == ReportStatus.Approved))
.Sum(r => r.Measurements.ValueA),
TotalApprovedValueB = a.Customers
.SelectMany(s => s.Reports.Where(r => r.Status == ReportStatus.Approved))
.Sum(r => r.Measurements.ValueB),
}).Where(a => a.Type == type)
}
}
This seems to work ok, but I'm wondering is there a cleaner way of expressing the total sums, as the code is quite repetitive. Something like:
a.Customers.SelectMany(s => s.Reports.Where(r => r.Status == ReportStatus.Approved)) {
r => {
TotalApprovedValueA = r.Sum(r.Measurements.ValueA),
TotalApprovedValueB = r.Sum(r.Measurements.ValueB),
}
}
The real object I'm working with is far more complex, so repeating the same initial query to select all the approved reports each time is making it quite difficult to follow. Is there a best-practice for avoiding this kind of repetition?
I'm using version 6 of EF-Core and Ardalis.Specification.EntityFrameworkCore.
Edit:
Added additional fields to summary class and query.

You can do that more effective:
var query =
from a in this.Query
from c in a.Customers
from r in c.Reports
from m in r.Measurements
where r.Status == ReportStatus.Approved && a.Type == type
group m by a.Id into g
select new AccountTable
{
Id = g.Key,
TotalApprovedValueA = g.Sum(x => x.ValueA),
TotalApprovedValueB = g.Sum(x => x.ValueB),
};
Note that probably your sample code has a bug, because I do not see query assignment. IQueryable is immutable.

Related

How to convert SQL subquery to linq lambda?

I have following SQL subqueries and want to convert them to linq lambda.
Count:
select count(*)
from ParentA
where orderStatus in ('Status1','Status2')
and orderNumer in (select orderNumber
from ChildB
where approval = 0)
Entities:
public class ParentA
{
public long ProductID { get; set; }
public string ProductName { get; set; }
public ChildB Order { get; set; }
public string orderStatus { get; set; }
public long orderNumer { get; set; }
}
public class ChildB
{
public long Id { get; set; }
public string Name { get; set; }
public long orderNumer { get; set; }
public bool approval { get; set;}
}
All columns:
select *
from ParentA
where orderStatus = 'Status1'
and orderNumer in (select orderNumber
from ChildB
where approval = 1)
Eg:
var query =_dbcontext.ParentA.Include().Where().Count();
After looking at your posted classes, it looks more like ChildB is the parent and ParentA is the child, with one ChildB per ParentA.
The following should be the method LINQ equivalents:
var result1 = db.ParentA
.Where(a =>
(a.orderStatus == "Status1" || a.orderStatus == "Status2")
&& !a.ChildB.approval
)
.Count();
var result2 = db.ParentA
.Where(a =>
(a.orderStatus == "Status1")
&& a.ChildB.approval
)
.ToList();
If you ChildB class has a ParentA collection not shown in your post, this could also be written as:
var result1 = db.ChildB
.Where(b => !b.approval)
.SelectMany(b => b.ParentA
.Where(a => a.orderStatus == "Status1" || a.orderStatus == "Status2")
)
.Distinct()
.Count();
var result2 = db.ChildB
.Where(b => b.approval)
.SelectMany(b => b.ParentA
.Where(a => a.orderStatus == "Status1")
)
.Distinct()
.ToList();
Here's a rough take of what you're looking for.
// Create a collection of some form with your statuses you want to check.
var statuses = new string[] {"Status1", "Status2"};
// Get your order numbers for the 'orderNumber in' portion of your query.
var orderNumbers = _dbcontext.ChildB.Where(x => x.approval == 0);
// Get the count using these collections.
var count = _dbcontext.ParentA.Where(x => statuses.Contains(x.orderStatus) && orderNumbers.Contains(x.orderNumber)).Count();
// Your 'All columns' query is slightly simpler, if you know you only need to filter by the one status instead of multiple...
var approvedOrderNumbers = _dbcontext.ChildB.Where(x => x.approval == 1);
var results = _dbcontext.ParentA.Where(x => x.orderStatus == "Status1" && approvedOrderNumbers.Contains(x.OrderNumber)).ToList();
Some things to note:
If you have a navigation property between ParentA and ChildB (which any properly designed database/DbContext should have, the LINQ would look different.
LINQ is always a little backwards from how SQL does in. As you can see, first you must build the collection you want to use for your in parameter, and then see if it contains the entity value.
I wrote this code freehand in a text editor, and I never use string arrays so I'm not even sure if that's the proper syntax to initialize one.
Based on the confirmation that you have navigation properties on ParentA, here's an example of how to use those:
var statuses = new string[] {"Status1", "Status2"};
var count = _dbcontext.ParentA.Where(x => statuses.Contains(x.orderStatus) && x.ChildBs.Any(y => y.approval == 0)).Count();
var results = _dbcontext.ParentA.Where(x => x.orderStatus == "Status1" && x.ChildBs.Any(y => y.approval == 1)).ToList();
Some additional notes:
If you were to share your actual goal, such as "Get the count of all the ParentAs where all of their ChildBs are approved," or, "Get all the columns from ParentA where any of their ChildBs are not approved," would allow us to better help you in writing the correct statements.
As it stands, these queries will get you:
A set of ParentA that contain at least one ChildB with approval of 0.
A set of ParentA that contain at least one ChildB with approval of 1.
If that's not what you want, then you need to include in plain English what your result set should look like.

Simplify querying SQL in Entity Framework Core

I have the following code:
public async Task<IEnumerable<Submission>> SelectSubmissionsAsync(string submitterId, IEnumerable<Group> groups)
{
var submissions = new List<Submission>();
var apps = context.Apps
.Select(a => new
{
Id = a.Id,
Member = a.MemberHistories.OrderByDescending(ash => ash.MemberChangeDate).FirstOrDefault().Member,
Owner = a.OwnerHistories.OrderByDescending(oh => oh.OwnerChangeDate).FirstOrDefault().Owner
})
.ToDictionary(x => x.Id, x => x.Member + x.Owner);
var subs = context.Submissions.ToList();
foreach (var sub in subs)
{
if (apps.ContainsKey((Guid)sub.AppId))
{
var value = apps[(Guid)sub.AppId];
var check = value.Contains(submitterId, StringComparison.InvariantCultureIgnoreCase) || groups.Any(g => value.Contains(g.Id, StringComparison.InvariantCultureIgnoreCase));
if (check)
submissions.Add(sub);
}
}
}
public class Submission
{
public Guid Id { get; set; }
public Application App { get; set; }
public Guid? AppId { get; set; }
}
public class App
{
public Guid Id { get; set; }
public string Identifier { get; set; }
public ICollection<MemberHistory> MemberHistories { get; set;}
public ICollection<OwnerHistory> OwnerHistories { get; set;}
}
Is there a way to simplify this code (avoid for loop for example)?
Ideally you should be able to construct a single query looking something like this:
var appInfo = context.Apps
.Select(a => new
{
Id = a.Id,
Member = a.MemberHistories.OrderByDescending(ash => ash.MemberChangeDate).FirstOrDefault().Member,
Owner = a.OwnerHistories.OrderByDescending(oh => oh.OwnerChangeDate).FirstOrDefault().Owner
})
.Where(appCriteria)
;
var submissions = context.Submissions
.Where(s => appInfo.Any(app => s.AppId == app.Id))
.ToList();
That will allow your app to build a single SQL command that filters the apps down to just the ones you want before bringing them back from the database.
Building checkCriteria will be complicated, because that's going to be based on the "OR"/Union of several criteria. You'll probably want to build a collection of those criteria, and then combine them using a strategy similar to what I've defined here. If you start with a collection of values including submitterId and groupIds, each criteria would be something like s => s.Member == val || s.Owner == val.
In order to create these expressions, you'll probably need to declare a class to represent the type that you're currently using an anonymous type for, so you have a name to associate with the generic arguments on your Expression types.

Left outer join using LINQ Query Syntax EF Core C#

I have a question in regards with the below,
Left outer join of two tables who are not connected through Foreign Key.
Order by the results matched in second table.
I would like this to be done in LINQ Query method syntax as I am adding lots of conditions depending on the input provided along with skip and limit.
If we have below Product and Favorite tables
So the output that I would like to have is:
meaning with the favorites as part of first set and which are not favorites should be behind them. Below are the tries that I did.
I am able to join the tables get the output but not sure how I can make sure that in the first page I get all the favs.
This answer was very near to what I thought but it gets the result and then does the ordering which will not be possible in my case as I am doing pagination and using IQueryable to get less data.
Group Join and Orderby while maintaining previous query
Open to any solutions to achieve the same.
[Table("Product")]
public class ProductModel
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid ProductId { get; set; }
public string ProductName {get; set;}
public bool IsFavorite { get; set; }
}
[Table("UserFavorite")]
public class UserFavoriteModel
{
[Required]
public Guid UserId { get; set; }
[Required]
public Guid Identifier { get; set; }
[Required]
public FavoriteType Type { get; set; }
}
// Gets products
private async Task<List<ProductModel>> GetProductsAsync(
Guid categoryId,
Guid subCategoryId,
int from,
int limit)
{
var query = _context.Products.AsQueryable();
if (!string.IsNullOrEmpty(categoryId))
query = query.Where(product => product.CategoryId == categoryId);
if (!string.IsNullOrEmpty(subCategoryId))
query = query.Where(product => product.SubCategoryId == subCategoryId);
query = query.Skip(from).Take(limit);
var products = await query.ToListAsync();
query = query.GroupJoin(
_context.Favorites.AsNoTracking()
.Where(favorite => favorite.Type == FavoriteType.FASHION)
// This user Id will come from context just adding for overall picture.
.Where(favorite => favorite.UserId == userId),
//This orderby if I add will not make any difference.
//.OrderByDescending(favorite => favorite.Identifier),
v => v.ProductId,
f => f.Identifier,
(product, fav) => new { product, fav }).
SelectMany(x => x.Fav.DefaultIfEmpty(),
(x, y) => SetFavorite(x.Project, y));
}
private static ProductModel SetFavorite(ProductModel v, UserFavoriteModel si)
{
v.IsFavorite = (si != null);
return v;
}
I would do something like this:
var query =
_context.Products.AsQueryable().Select(p => new ProductModel {
ProductId = p.ProductId,
ProductName = p.ProductName,
IsFavorite =
_context.Favorites.Any(f =>
f.Identifier = p.ProductId &&
f.Type == FavoriteType.FASHION &&
f.UserId == userId
)
}).OrderByDescending(favorite => favorite.Identifier);

What's wrong with my translation from Linq Query Expression to Method-Based Query?

I have two Aggregate Roots: Student and Test, and each student could have 0..1 test. I am using EF Core 3.1 to perform a left outer join query to fetch students with her own test. Since the test could be null for a student, and EF Core dosen't support database GroupJoin, I write down my query as this:
var studentsWithTest = from o in dbContext.Students
join x in dbContext.Tests on o.TestId
equals x.Id into tests
from x in tests.DefaultIfEmpty()
select new {o.Student, Test = x};
It works. However, what I am really interested is the equivalent method-based query performing the same fuction. I try to translate it as this:
var studentsWithTest = dbContext.Students.SelectMany(o =>
dbContext.Tests
.Where(x => x.Id == o.TestId).DefaultIfEmpty()
.Select(x => new {o.Student, Test = x}));
But this code causes run-time Exception:
Processing of the LINQ expression '(ProjectionBindingExpression:
Student)' by 'RelationalProjectionBindingExpressionVisitor' failed.
This may indicate either a bug or a limitation in EF Core. See
https://go.microsoft.com/fwlink/?linkid=2101433 for more detailed
information.
Dose that mean I have made something wrong during the translation? Could you guys help me to figure that out? Thanks.
For this error, the link in the error message will be the best explanation.
To solve this , add ToList() to dbContext.Students as follow :
var studentsWithTest = dbContext.Students.ToList().SelectMany(o =>
dbContext.Tests
.Where(x => x.Id == o.TestId).DefaultIfEmpty()
.Select(x => new {o.Student, Test = x}));
However, for the one-to-one relationship in ef core,you can also use Include is actually the easiest way, please refer to the following writing:
public class Student
{
public int Id{ get; set; }
public string Name { get; set; }
public int? TestId{ get; set; }
public Test Test{ get; set; }
}
public class Test
{
public int Id{ get; set; }
public string Name { get; set; }
public Student Student { get; set; }
}
Linq:
var studentsWithTest = dbContext.Students.Include(x => x.Tests)
.Select(x => new { x, Test = x.Tests });
Update
As you comment said, you can try this code:
var studentsWithTest = dbContext.Students
.SelectMany(o => dbContext.Tests.Where(x => x.Id == o.TestId).DefaultIfEmpty(), (o, x) => new
{
o.Student,
Test = x
});
You can also refer to this.

LINQ Sub Query In Result Entity Framework 5

I've the following class
public class Interview
{
public int Id { get; set; }
public ICollection<InterviewSlot> Slots { get; set; }
}
public class InterviewSlots
{
public int Id { get; set; }
public Candidate Candidate { get; set; }
}
public class Candidate
{
public int Id { get; set; }
}
I want something like this,
var candidates = _DbContext.Interviews.Where(i => i.Id == Id).Select(s => s.Slots.Select(c => c.Candidate).ToList();
I don't want to use the InterviewSlots or the Candidate object
I want to get all the Candidates in a interview.
What would the LINQ be for this??
I'm thinking it may be along the lines of something like this in linq:
var candidates = _DbContext.Interviews.Where(i => i.Id == id)
.SelectMany(interview => interview.Slots)
.Select(slot => slot.Candidate)
.ToList();
tho, without seeing exactly how you plan to use it, quite a tricky one to answer.
I don't really understand your question
What would the LINQ be for this??
But here's what you need in order to get all candidates in an interview.
Without null checking.
var interview = _DbContext.Interviews.Where(i => i.Id == Id).Single();
var candidates = interview.Slots.Select(s => s.Candidate);
With null checking
var interview = _DbContext.Interviews.Where(i => i.Id == Id).SingleOrDefault();
if (interview != null)
var candidates = interview.Slots.Select(s => s.Candidate);
In one line
_DbContext.Interviews.Where(i => i.Id == Id)
.Single()
.Slots.Select(s => s.Candidate);

Categories

Resources