Entity Framework Core: Group by using calculated field - c#

I'm trying to write a query. What I have so far is shown below.
var x = from d in DbContext.StorageDetails
let state = (d.ReleaseDate.HasValue && d.ReleaseDate < date) || (d.TakeOrPayEndDate.HasValue && d.TakeOrPayEndDate < date) ?
StorageState.Closed :
(d.RailcarNumber == null || d.RailcarNumber == string.Empty) ?
d.TakeOrPayStartDate.HasValue ? StorageState.TakeOrPay : StorageState.Open :
(d.ArrivalDate.HasValue && d.ArrivalDate <= date) ? StorageState.Filled : StorageState.EnRoute
group d by new
{
d.CustomerId,
d.Customer.Name,
d.LocationId,
d.Location.City,
d.Location.State
} into g
select new
{
// ...
};
The part that's giving me trouble is that I want to include the calculated state value with each item. I don't want to group by this value but I want to be able to sum it.
// Note: This is after my group statement
select new
{
// state is a let variable and not part of x!
TotalOpen = g.Sum(x => x.state == StorageState.Open),
TotalClosed = g.Sum(x => x.state == StorageState.Closed),
// Etc.
};
Is there a way to do this? I don't seem able to select my own set of columns prior to group by. How can I insert this calculated field into each item?
Note: StorageState is an enum. I can just as easily cast it to an int in all of these expressions. I can figure that part out, but figuring it out is separate from my question here.

I don't know how you would write this as a query expression, nor if EF can translate that expression to sql. I would do something like the following;
var query = ....
// Select all the values you need
.Select(d => new {
d.CustomerId,
d.Customer.Name,
d.LocationId,
d.Location.City,
d.Location.State,
....
state = [insert expression]
})
// group by this key
.GroupBy(d => new {
d.CustomerId,
d.Name,
d.LocationId,
d.City,
d.State
},
// and define the final result set
(d, g) => new {
d.CustomerId,
d.Name,
d.LocationId,
d.City,
d.State,
TotalOpen = g.Sum(x => x.state == StorageState.Open ? 1 : 0),
TotalClosed = g.Sum(x => x.state == StorageState.Closed ? 1 : 0)
});
I think the sql EF will generate, will put the first select in the from clause, surrounded by your typical select / group by statement.

What about this:
TotalOpen = g.Sum(x => (x.ReleaseDate.HasValue && x.ReleaseDate < date) || (x.TakeOrPayEndDate.HasValue && x.TakeOrPayEndDate < date) ?
StorageState.Closed :
(x.RailcarNumber == null || x.RailcarNumber == string.Empty) ?
x.TakeOrPayStartDate.HasValue ? StorageState.TakeOrPay : StorageState.Open :
(x.ArrivalDate.HasValue && x.ArrivalDate <= date) ? StorageState.Filled : StorageState.EnRoute
== StorageState.Open? 1 : 0)
You would repeat the entire expression four times.

There seems to be two things happening here:
StorageDetails is queried from DbContext and somehow for every item(d) found, there's some execution that is being processed to determine the value of state...
state is determined for each d and then all the d's are grouped, and then after only few properties of d are selected...
This might be another approach:
var x = DbContext.StorageDetails
.GroupBy(d => new
{
d.CustomerId,
d.Name,
d.LocationId,
d.City,
d.State
}).Select(d => new
{
d.CustomerId,
d.Name,
d.LocationId,
d.City,
d.State,
d.state = getState(d)
}).ToList();
int getState(d)
{
return (d.ReleaseDate.HasValue && d.ReleaseDate < date) || (d.TakeOrPayEndDate.HasValue && d.TakeOrPayEndDate < date) ?
StorageState.Closed :
(d.RailcarNumber == null || d.RailcarNumber == string.Empty) ?
d.TakeOrPayStartDate.HasValue ? StorageState.TakeOrPay : StorageState.Open :
(d.ArrivalDate.HasValue && d.ArrivalDate <= date) ? StorageState.Filled : StorageState.EnRoute;
}
var TotalOpen = x.Count(x => x.state == StorageState.Open),
var TotalClosed = x.Count(x => x.state == StorageState.Closed)
Basically you first get your data from database group it. After grouping it, you then select specific properties and introduce one property called state... But state is calculated, using another function which takes a row from database return a value from enum StorageState like you have done on the question. Then return the result as list.
Only after you have your list will it be easier to check which of the rows returned contains an open or closed...
Now you can do normal looping and loop through your x and calculate TotalClosed and TotalClosed.

Related

How can i get Max Date in Linq

I have the following linq expression to compare users parameter to Max(EndDate) field?. I have multiple EndDate's but i want to get the last one.
Here is
Product 1
EndDate
2018-09-30 23:59:59.000
2019-09-30 23:59:59.000
Product 2
EndDate
2019-09-30 23:59:59.000
2019-12-31 23:59:59.000
When user enter 09/2019 I need to return only Product 1 since the Max(EndDate) == the value entered by the user which is 09/2009 in MM/yyyy format, currently it displays both product 1 & product 2.
In my model i have checkBillingMonth as bool
public bool checkBillingMonth { get; set; }
Here my linq expression looks like this but it return both prodcut 1 and product 2
var billingProductList = Context.Product.AsNoTracking()
.Select(p => new ProductDTO
{
checkBillingMonth = p.Billing
.Any(b => b.EndDate.Month == request.FromDate.Month &&
b.EndDate.Year == request.FromDate.Year)
}).ToList();
How can i modify the above linq expression to get Max(EndDate) and compare to request.FromDate?
You can try the below code.
var billingProductList = Context.Product.AsNoTracking()
.Select(p => new ProductDTO
{
checkBillingMonth = GetCheckBillingMonth(p.Invoices, request.FromDate)
// I'm not sure why you were having the condition related to min date
}).ToList();
Private method:
private bool GetCheckBillingMonth(List<Invoice> invoices, DateTime fromDate)
{
if (invoices == null || invoices.Count == 0) return false;
var latestInvoiceEndDate = invoices.OrderByDescending(x => x.EndDate).FirstOrDefault()?.EndDate;
return latestInvoiceEndDate != null && latestInvoiceEndDate.Month == fromDate.Month && latestInvoiceEndDate.Year == fromDate.Year;
}
If I'm understanding the question properly, you want to compare to the last invoice? Then you should flip your primary result set from Product to Invoice. Filter the latest invoices. Then select the related product details.
Context.Invoice
.AsNoTracking()
.Where(i => i.EndDate == i.Product.Invoices.Max(i2 => i2.EndDate))
.Select(i => new ProductDTO{
checkBillingMonth = i.EndDate.Month == request.FromDate.Month &&
i.EndDate.Year == request.FromDate.Year &&
!request.FromDate.Equals(DateTime.MinValue)),
// plus other properties from i.Product
});
;

How to use temp local data when creating Anonymous type?

I'm getting some Entities from EF, which I iterate and creating Anonymous type objects such as:
var payments = ctx.Payments.ToList();
var data = ctx.Activities.OrderBy(p => p.ID).ToList().Select(p => new
{
ID = p.ID,
Date = p.Date?.ToString("dd/MM/yyyy"),
PaymentMethod = p.PaymentMethods != null ? p.PaymentMethods.Description : "",
ActivityStatusID = payments.Where(q => q.ActivityID == p.ID && !q.Paid).Count() == 0 ? 1 : .. // I need some other check
}).ToList();
Now, I'd like to check the payments.Where(q => q.ActivityID == p.ID && !q.Paid).Count() several times before set a value.
Such as:
if .Count() == 0, value 1
if .Count() == 1, value 8
if .Count() > 1, value 10
else 87
and so on. I won't do somethings like this:
ActivityStatusID = payments.Where(q => q.ActivityID == p.ID && !q.Paid).Count() == 0 ? 1 : payments.Where(q => q.ActivityID == p.ID && !q.Paid).Count() == 1 ? 8 : payments.Where(q => q.ActivityID == p.ID && !q.Paid).Count() > 1 ? 10 : 87
Is there any way to do payments.Where(q => q.ActivityID == p.ID && !q.Paid).Count() once for each p, and than evalutate the conditions?
People suggested to convert your query into query syntax, which would enable your to use a let statement to create a variable in which you could save count.
If you investigate what let does, it adds one column value to your result. Every row in your result has the same let value in this column. See this answer on stackoverflow
Keep your query in Method Syntax
If you want to keep your query in method syntax, simply add a Count to your anonymous type:
var result = ctx.Activities
.OrderBy(p => p.ID)
.Select(p => new
{
Id = p.Id,
Date = p.Date?.ToString("dd/MM/yyyy"),
PaymentMethod = p.PaymentMethods != null ? p.PaymentMethods.Description : "",
PaidCount = payments
.Where(q => q.ActivityID == p.ID && !q.Paid)
.Count();
})
.Select(p => new
{
Id = p.Id,
Date = p.Date,
ActivityStatusId =
{
// count == 0 => 1
// count == 1 => 8
// count > 1 => 10
// count < 0 => 87
if (p.PaidCount < 0) return 87;
switch (p.PaidCount)
{
case 0:
return 0;
case 1:
return 8;
default:
return 10;
}
},
});
Note, the switch is only possible because you brought your complete ctx.Activities to local memory using the ToList.
Improved efficiency
You do a ToList before your select. This means that your complete ctx.payments are materialized in local memory into a List, before you start selecting and counting items in your sequence.
If ctx.Payments is from an external source, like a database, or a file, then ctx.Payments is an IQueryable instead of an IEnumerable. Fetching your complete Payments to local memory is not an efficient approach.
Advise: Whenever you have an IQueryable, try to keep it IQueryable as long as possible. Your source data provider can process your queries much more efficiently than your local processor. Only materialize it to local memory if your source data provider can't process it anymore, for instance because you need to call local procedures, or because there is nothing to process anymore.
Furthermore, don't move values to local memory that you don't plan to use. Only Select the properties that you actually will use in your local memory.
One improvement would be:
var result = ctx.Payments.Select(payment => new
{ // select only the properties you plan to use locally:
Id = payment.Id,
Date = payment.Date,
PaymentMethod = payment.PaymentMethods?.Description,
PaidCount = ctx.Payments
.Where(q => q.ActivityID == p.ID && !q.Paid)
.Count(),
})
.OrderBy(fetchedPaymentData => fetchedPaymentData.Id)
// from here you need to move it to local memory
// Use AsEnumerable instead of ToList
.AsEnumerable()
.Select(fetchedPaymentData => new
{
Id = fetchedPaymentData.Id,
PaymentMethod = fetchedPaymentData.PaymentMethod ?? String.Empty,
ActivityStatusId = {...}
});
AsEnumerable is more efficient than ToList, especially if you don't need all items at once. For instance if you would end with FirstOrDefault, or only Take(5), then it would be a waste to move all items to local memory.
Finally: with some trying you can get rid of the switch statement, thus allowing your DBMS to calculate the ActivityStatusId. But as the selecting of your source data and the transport of the selected data to local memory is the slower part of your complete query, I doubt whether this would lead to shorter execution time. The switch surely makes your requirement better readable, especially if your numbers 1 / 8 / 87 are put into enums.
I would change the query from a lambda expression to an ordinary query. Then use the let syntax to set a variable for each iteration. Something like this:
var payments = ctx.Payments.ToList();
var data = (
from p in ctx.Activities.ToList()
orderby p.ID
let paymentCount = payments.Where(q => q.ActivityID == p.ID && !q.Paid).Count()
select new
{
ID = p.ID,
Date = p.Date?.ToString("dd/MM/yyyy"),
PaymentMethod = p.PaymentMethods != null ? p.PaymentMethods.Description : "",
ActivityStatusID = paymentCount == 0 ? 1 : .. // I need some other check
}
).ToList();
And btw you can do this part diffrently as well. This:
payments.Where(q => q.ActivityID == p.ID && !q.Paid).Count()
You can write like this:
payments.Count(q => q.ActivityID == p.ID && !q.Paid)

How to sum a field on linq that has a where clause which pulls data that returns nulls too?

I have a LINQ query, which groups by the company's Warehouse descriptions. Now, I had to sum the items' weight for the month. But then I have another field, which sums the total weight of scrap, which is found by a field that that contains the word "SCRP". I get the infamous object reference error is not set to an instance of an object error when I do this. I assume it's because there are nulls or something. The field contains other values or nulls.
This is the query that I have. It runs perfectly until I try to add the UserSequence bit:
P.s the scrap percentage doesn't work either, but I assume this is because of the same issue.
var fistDayofPreviousMonth = DateTime.Today.AddMonths(-4);
var testScrapQuery = from s in mapicsSession.Query<InventoryHistory>()
where s.PostedTimestamp > fistDayofPreviousMonth
orderby s.Warehouse.Description
group s by s.Warehouse.Description
into test
let tw = mapicsSession.Query<InventoryHistory>()
.Where(x => x.Warehouse.Description == test.Key)
.Sum(x => x.Item.Weight)
let sw = mapicsSession.Query<InventoryHistory>()
.Where(x => x.Warehouse.Description == test.Key
&& x.UserSequence == "SCRP")
.Sum(x => x.Item.Weight)
select new
{
Warehouse = test.Key,
TotalWeight = tw,
ScrapWeight = sw
//ScrapPercentage = (sw / tw) * 100
};
You can fix the first issue by coalescing the value (now it takes 0 as a value if x or x.Item is null):
.Sum(x => x?.Item?.Weight ?? 0)
Or with an additional Where:
.Where(x => x != null && x.Item != null)
.Sum(x => x.Item.Weight)
And I guess this could do for your percentage calculation (prevent division by 0):
ScrapPercentage = tw == 0 ? 100 : (sw / tw) * 100

Code First Entity Framework Linq Statement Returning Missing Records

I have the following query:
orderMessageEmail.MessageChain = DbContext.Current
.Messages
.Where(c =>
c.OrderId == orderMessageEmail.OrderId &&
c.IsPublic &&
c.MessageId != orderMessageEmail.MessageId &&
c.MessageId < orderMessageEmail.MessageId
)
.Select(c => new OrderMessageChain()
{
CreateDateTime = c.CreateDateTime,
MessageId = c.MessageId,
Message = c.MessageData,
UserFirstName = c.User.FirstName,
UserLastName = c.User.LastName,
CustomerFirstName = c.CustomerAccountPerson.FirstName,
CustomerLastName = c.CustomerAccountPerson.LastName,
SentFrom = c.SentFrom
})
.OrderByDescending(c => c.MessageId)
.Take(10)
.ToList();
The problem I'm running into is that whenever c.User is null it doesn't return ANY record for the OrderMessageChain
I'd like to just have it return UserFirstName and UserLastName as empty strings instead of completely eliminating that OrderMessageChain from the list.
Does this make sense?
One more thing..
Simply testing this query:
var t = DbContext.Current
.Messages
.Include("User")
.Where(c =>
c.MessageId < 138120 &&
c.OrderId == 170496 &&
c.IsPublic)
.ToList();
When I manually execute the same query in the DB i'm shown 3 records, however t is showing zero.
I always thought Include worked as a Left Join - is that not the case?
One more thing...
OK so i think i'm starting to realize what is going on here..
I didn't realize this, but it appears that the DBA setup the DB field UserId on the Message Table to NOT be nullable, however, in the case when there isn't a user record the UserId field contains a 0 (zero) value, instead of null... argh...
I think this is leading to code first to believe it should perform an inner join instead of a left join per here
So i'm not quite sure how to fix this.. is there anyway I can force code first to somehow perform a left join on that navigation property instead of an inner?
You should be able to select the data you need, convert it to an IEnumerable and do the mapping in-memory, something like (the untested);
orderMessageEmail.MessageChain = DbContext.Current
.Messages
.Where(c =>
c.OrderId == orderMessageEmail.OrderId &&
c.IsPublic &&
c.MessageId != orderMessageEmail.MessageId &&
c.MessageId < orderMessageEmail.MessageId
)
.OrderByDescending(c => c.MessageId)
.Take(10)
.Select(c => new {
c, u = c.User, cap = c.CustomerAccountPerson
}).
.AsEnumerable()
.Select(c => new OrderMessageChain()
{
CreateDateTime = c.c.CreateDateTime,
MessageId = c.c.MessageId,
Message = c.c.MessageData,
UserFirstName = c.u == null ? "" : c.u.FirstName,
UserLastName = c.u == null ? "" : c.u.LastName,
CustomerFirstName = c.cap == null ? "" : c.cap.FirstName,
CustomerLastName = c.cap == null ? "" : c.cap.LastName,
SentFrom = c.c.SentFrom
})
.ToList();

.Where inside a .Select , how to select only record that match the where clause:-

I have the following two statements :-
var isadminByuser = tms.SecurityRoles.Where(a => a.Name.ToLower() == "administrator")
.Select(a=>a.SecurityRoleUsers.Where(a2 => a2.UserName.ToLower() == user.ToLower()));
if (isadminByuser.Count() >= 1) { return true;}
&
var adminByGroup = tms.SecurityRoles.Where(a => a.Name == "Administrator")
.SingleOrDefault().Groups
.Select(a2 => a2.TMSUserGroups
.Where(a3 => a3.UserName.ToLower() == user.ToLower()));
bool isadminByGroup = adminByGroup.Count() >= 1;
The first var isadminByuser will always have elements (will always be >=1), even if the where clause a3.UserName.ToLower() == user.ToLower())) is false, while the other var will have a count of zero is the where clause (a3.UserName.ToLower() == user.ToLower())) is false. So why will the first var never have a count of zero?
Thanks
The answer to the question asked is that you are selecting an IQueryable<SecurityRoleUsers> when you select
a.SecurityRoleUsers.Where(a2 => a2.UserName.ToLower() == user.ToLower())
which may or may not have a count of 0, but the containing query will return one of these IQueryables for each SecurityRole that matches Name = "administrator", hence the count will always be 1+ if there is at least one matching SecurityRole
Update:
// first get SecurityRoles
bool isadminByuser = tms.SecurityRoles.Where(a => a.Name.ToLower() == "administrator")
// now you're only interested in the related SecurityRoleUsers
.SelectMany( a => a.SecurityRoleUsers )
// now check if any of the SecurityRoleUsers meet your criteria
.Any( sru => sru.UserName.ToLower() == user.ToLower() );
Could you observe sql query differences from ms-sql-profiler
tms.SecurityRoles.Where(a => a.Name.ToLower() == "administrator")
.Select(a=>a.SecurityRoleUsers.Where(a2 => a2.UserName.ToLower() == user.ToLower()));
***
tms.SecurityRoles.Select(a=>a.SecurityRoleUsers.Where(a2 => a2.UserName.ToLower() == user.ToLower())).Where(a => a.Name.ToLower() == "administrator");

Categories

Resources