I have a complex where clause in my EF linq statement which repeats a subquery expression, on _db.OPESRRecoveryElements, but with different parameters, one of which is depending on records from the main entity, OPCases/OPCaseDto.
The query as it is works, but its hard for people to read. Ideally I'd like to be able to create an expression which could be re-used at the 3 necessary points and would still allow it to execute as a single, server-side SQL statement.
Is there a way to create an Expression / IQueryable definition which can be used for a subquery like this?
List<OPCaseDto> opCases = await _db.OPCases
.ProjectTo<OPCaseDto>(_autoMapperConfig, null, requestedExpands)
.Where(c =>
c.OPStatusId == OPStatusIds.AwaitingRecoveryElement
&& (
(c.OPCategoryLetter == "B"
// Only need a gross pensionable element if there is an outstanding gross pensionable figure
&& (c.GrossOverpaidPensionable - c.GrossRecoveredPensionable == 0
|| _db.OPESRRecoveryElements.Any(e => !e.NonPensionable && e.OPRecoveryMethod.OPTypeLetter == "G"
&& !e.OPRecoveryPlans.Any(rp
=> (rp.RecoveryStatus == OPRecoveryStatuses.NotStarted || rp.RecoveryStatus == OPRecoveryStatuses.InRecovery)
&& rp.AssignmentNo == c.RecoveryAssignmentNo)))
// Only need a gross non-pensionable element if there is an outstanding gross non-pensionable figure
&& (c.GrossOverpaidNonPensionable - c.GrossRecoveredNonPensionable == 0
|| _db.OPESRRecoveryElements.Any(e => e.NonPensionable && e.OPRecoveryMethod.OPTypeLetter == "G"
&& !e.OPRecoveryPlans.Any(rp
=> (rp.RecoveryStatus == OPRecoveryStatuses.NotStarted || rp.RecoveryStatus == OPRecoveryStatuses.InRecovery)
&& rp.AssignmentNo == c.RecoveryAssignmentNo))))
|| (c.OPCategoryLetter == "D"
// Don't need to check for an outstanding net figure - if the case is net and isn't complete, there will be one!
&& _db.OPESRRecoveryElements.Any(e => e.OPRecoveryMethod.OPTypeLetter == "N"
&& !e.OPRecoveryPlans.Any(rp
=> (rp.RecoveryStatus == OPRecoveryStatuses.NotStarted || rp.RecoveryStatus == OPRecoveryStatuses.InRecovery)
&& rp.AssignmentNo == c.RecoveryAssignmentNo)))
)
)
.AsNoTracking()
.ToListAsync();
If it wasn't for the c.RecoveryAssignmentNo part, I could easily create an expression like:
public Expression<Func<OPESRRecoveryElement, bool>> NoActiveRecoveryPlans(string opType, bool nonPen)
{
return e => e.OPRecoveryMethod.OPTypeLetter == opType
&& e.NonPensionable == nonPen
&& !e.OPRecoveryPlans.Any(rp
=> (rp.RecoveryStatus == OPRecoveryStatuses.NotStarted || rp.RecoveryStatus == OPRecoveryStatuses.InRecovery));
}
and use it like:
(c.OPCategoryLetter == "B"
// Only need a gross pensionable element if there is an outstanding gross pensionable figure
&& (c.GrossOverpaidPensionable - c.GrossRecoveredPensionable == 0
|| _db.OPESRRecoveryElements.Any(NoActiveRecoveryPlans("G", false)))
and it would get executed before the query to get the OPCases.
I could also fetch all the OPCaseDto records and OPESRRecoveryElements as separate queries and filter in memory, but I don't want to do that.
If I add a parameter to the function, string assignmentNo, I (unsurprisingly) get an error - "Unable to cast object of type 'System.Linq.Expressions.InstanceMethodCallExpression3' to type 'System.Linq.Expressions.LambdaExpression'"
I'm trying to build a LINQ query that executes as values change, however I only want to bottom 4 statements relating to price and surface area to run on the condition that a certain checkbox on my Windows form is ticked. My code is below
var userSearchQuery =
from sale in saleData
where checkedCities.Contains(sale.City)
&& checkedBedrooms.Contains(sale.Bedrooms)
&& checkedBathrooms.Contains(sale.Bathrooms)
&& checkedHouseTypes.Contains(sale.HouseType)
&& minPrice <= sale.Price
&& maxPrice >= sale.Price
&& minSurfaceArea <= sale.SurfaceArea
&& maxSurfaceArea >= sale.SurfaceArea
select sale;
Can anyone help with the best way to do this please
What you could do is make the base query just as it is. So just remove last 4 conditions that you wish to dinamically add depending on some condition from UI. You will see that your query is of type IQueryable.
var userSearchQuery =
from sale in saleData
where checkedCities.Contains(sale.City)
&& checkedBedrooms.Contains(sale.Bedrooms)
&& checkedBathrooms.Contains(sale.Bathrooms)
&& checkedHouseTypes.Contains(sale.HouseType);
Do not select anything yet. Now add your condition depending on UI.
if(checkBox1.Checked)
userSearchQuery = userSearchQuery.Where(s => minPrice <= s.Price);
if(checkBox2.Checked)
userSearchQuery = userSearchQuery.Where(s => maxPrice => s.Price);
if(checkBox3.Checked)
userSearchQuery = userSearchQuery.Where(s => minSurfaceArea => s.SurfaceArea);
if(checkBox4.Checked)
userSearchQuery = userSearchQuery.Where(s => maxSurfaceArea => s.SurfaceArea);
Finally execute the query by calling ToList().
var results = userSearchQuery.Select(s => s).ToList();
You can stack the queries, so first create an IEnumerable for all cases and then add additional queries to previous IEnumerable only when your checkbox is checked.
var userSearchQuery = from sale in saleData
where checkedCities.Contains(sale.City)
&& checkedBedrooms.Contains(sale.Bedrooms)
&& checkedBathrooms.Contains(sale.Bathrooms)
&& checkedHouseTypes.Contains(sale.HouseType)
select sale;
if (checkbox.IsChecked)
{
userSearchQuery = from sale in userSearchQuery
where minPrice <= sale.Price
&& maxPrice >= sale.Price
&& minSurfaceArea <= sale.SurfaceArea
&& maxSurfaceArea >= sale.SurfaceArea
select sale;
}
You could use the fact that 'true' returns the result as follows:
var userSearchQuery =
from sale in saleData
where checkedCities.Contains(sale.City)
&& checkedBedrooms.Contains(sale.Bedrooms)
&& checkedBathrooms.Contains(sale.Bathrooms)
&& checkedHouseTypes.Contains(sale.HouseType)
&& (*some condition is checked*) ? (minPrice <= sale.Price && maxPrice >= sale.Price && minSurfaceArea <= sale.SurfaceArea && maxSurfaceArea >= sale.SurfaceArea) : true
select sale;
I have tested the syntax, but not the execution so let me know if it doesn't work as expected.
For reading reference about the '?' operator:
?: Operator (C# Reference)
EDITED:
As per apocalypse's comment, there is no need to check the condition multiple times.
I have a List property that I am setting like so:
testCard.LstSummaries =
db.Summaries.Where(
x =>
(x.AID == aId || x.AInformation.RegNumber == aRegNumber) && DbFunctions.TruncateTime(x.Day) == DateTime.Today.Date &&
x.deleted == false).ToList();
Then I have a conditional statement:
if (testCard.LstSummaries.Count > 0)
{
if (
testCard.LstSummaries.All(
x =>
(x.AID == aId || // ERROR HAPPENS ON THIS LINE
x.AInformation.RegNumber == aRegNumber) &&
DbFunctions.TruncateTime(x.Day) == DateTime.Today.Date && x.deleted == false))
{
// .... do something
}
I get an error:
This function can only be invoked from LINQ to Entities.
I want to avoid to make multiple calls to the database.. furthermore testCard.LstSummaries already has the values I am looking for.. but if I do this:
if (testCard.LstSummaries.Count > 0)
{
if (
db.Summaries.All(
x =>
(x.AID == aId || // NO ERROR
x.AInformation.RegNumber == aRegNumber) &&
DbFunctions.TruncateTime(x.Day) == DateTime.Today.Date && x.deleted == false))
{
// .... do something
}
I feel like making this call to the database is pointless because I would be retrieving the same results that are already stored in testCard.LstSummaries, but I can't invoke .All() because it's not LINQ to Entities.
Is there a workaround for this?
Problem is with DbFunctions.TruncateTime(x.Day), because it is converted to sql on runtime. Try to check without it.
I have an object with many fields, but I want to order on 3 columns (boolean [IsValid], int [Count], and date [CreateDate]), conditionally dependent on the boolean.
Data:
|| ID || IsValid || Count || CreateDate ||
==========================================
|| 1 || True || 3 || 2016-05-01 ||
|| 2 || True || 2 || 2016-07-12 ||
|| 3 || False || NULL || 2015-06-16 ||
|| 4 || False || 1 || 2015-01-01 ||
The order I want is:
1) Valid items first
2) If valid, order by Count, else by CreateDate (essentially (o.IsValid) ? o.Count : o.CreateDate, but that didn't work), so that the order would be
|| ID || IsValid || Count || CreateDate ||
==========================================
|| 2 || True || 2 || 2016-07-12 ||
|| 1 || True || 3 || 2016-05-01 ||
|| 4 || False || 1 || 2015-01-01 ||
|| 3 || False || NULL || 2015-06-16 ||
If something was once valid, it could be invalidated again, but would not reset the Count.
I've tried doing things like:
db.OrderBy(o => new { o.IsValid, o.Count }).ThenBy(o => new { o.IsValid, o.CreateDate })
or
db.OrderByDescending(o => o.IsValid).ThenBy(o => new { o.SortOrder, o.CreateDate })
and other combinations, but I don't know how to get the ordering to work as desired.
Anyone able to help?
Because LINQ queries are lazily evaluated, you can accomplish this with a couple of different queries that get concatenated together. I'm not sure how efficient this would be for you, but it accomplishes your goal.
class Program
{
static void Main(string[] args)
{
IEnumerable<OrderObject> db = new List<OrderObject>
{
new OrderObject { Count=1, CreateDate=DateTime.Now.Subtract(TimeSpan.FromDays(0)), IsValid=true },
new OrderObject { Count=2, CreateDate=DateTime.Now.Subtract(TimeSpan.FromDays(1)), IsValid=false },
new OrderObject { Count=3, CreateDate=DateTime.Now.Subtract(TimeSpan.FromDays(2)), IsValid=false },
new OrderObject { Count=4, CreateDate=DateTime.Now.Subtract(TimeSpan.FromDays(3)), IsValid=true },
new OrderObject { Count=5, CreateDate=DateTime.Now.Subtract(TimeSpan.FromDays(4)), IsValid=false },
};
var validItemsOrderedByCount = (from obj in db
where obj.IsValid
orderby obj.Count
select obj);
var nonValidItemsOrderedByDateCreated = (from obj in db
where obj.IsValid == false
orderby obj.CreateDate
select obj);
var combinedList = validItemsOrderedByCount
.Concat(nonValidItemsOrderedByDateCreated)
.ToList();
}
}
class OrderObject
{
public bool IsValid { get; set; }
public DateTime CreateDate { get; set; }
public int Count { get; set; }
}
Or if you prefer LINQ method syntax, this should work.
var validItemsMethodSyntax = db.Where(x => x.IsValid).OrderBy(x => x.Count);
var nonValidItemsMethodSyntax = db.Where(x => x.IsValid == false).OrderBy(x => x.CreateDate);
var combinedMethodSyntax = validItemsMethodSyntax
.Concat(nonValidItemsMethodSyntax)
.ToList();
Or using Union and method syntax with one variable as requested.
var usingUnion = db.Where(x => x.IsValid)
.OrderBy(x => x.Count)
.Union(db.Where(x => x.IsValid == false).OrderBy(x => x.CreateDate))
.ToList();
The simplest I could suggest is:
db.OrderByDescending(o => o.IsValid)
.ThenBy(o => o.IsValid ? DbFunctions.AddSeconds(DateTime.MinValue, o.Count) : o.CreateDate)
The idea is to convert both columns CreateDate and Count to same type, then order based on it's value. For linq to objects I would convert both to integral representation:
.ThenBy(o => o.IsValid ? o.Count : o.CreateDate.Ticks)
but this will not work for EF since there is it cannot map .Ticks to sql query. Also I didn't found graceful way to represent datetime as integer value, so, another way is to convert existing integer value to datetime. Now we have both columns represented as datetime and can easily order by it's value.
Update
Just did some tests and found that DbFunctions.AddSeconds(DateTime.MinValue, o.Count) is represented as datetime2, which could be lesser then January 1, 1753 (minimum for datetime). Values of this expression are fall in range [01.01.01 00:00:00 .. 19.01.0069 3:14:07] but limited by .Count >= 0 as negative values will produce overflow error.
Let's support negative values for .Count:
DbFunctions.AddSeconds(new DateTime(70, 1, 1), o.Count)
Values of this expression are fall in range [13.12.0001 20:45:53 .. 20.01.0138 3:14:07], which is definitely lesser then .CreateDate values (I guess, your min value for .CreateDate is in current or past century, i.e. .CreateDate is much bigger then 20.01.138, until you write software in ancient ages). So that is why we could throw .OrderByDescending(o => o.IsValid) away.
The final answer to your question is a single order by:
db.OrderBy(o => o.IsValid
? DbFunctions.AddSeconds(new DateTime(70, 1, 1), o.Count)
: o.CreateDate)
Valid values will go first as they will have lesser datetime2 values, then invalid ones. Valid values will be ordered by ascending .Count.
However you could easily manage order direction as well as who goes first, valid or invalid. For example, change year of base date for .Count to 7000 instead of 70, multiply .Count by -1 and so on.
You can do this with ternary operators as juharr mentioned in the comments:
db.ToList()
.OrderBy(e => e.IsValid)
.ThenBy(e => e.IsValid ? e.Count : 0)
.ThenBy(e => e.IsValid ? DateTime.MinValue : e.CreateDate);
Keep in mind that if you are doing any filtering, do it before the .ToList() to minimize the results coming back from the data layer.
Well,
I am not sure if I am wrong or if the linq parsed mistakes, but the following linq query returns what I DON'T want if I don't use additional parenthesis.
int? userLevel;
Guid? supervisorId;
List<int> levelsIds = new List<int>();
string hierarchyPath;
// init the vars above
// ...
var qry = from f in items
where userLevel.HasValue
? (f.IsMinLevelIdNull() || (levelsIds.Contains(f.MinLevelId)))
: true
&& supervisorId.HasValue
? (f.IsSupervisorIdNull() || (!f.ChildrenVisibility && (f.SupervisorId == supervisorId.Value))
|| (f.ChildrenVisibility && (hierarchyPath.IndexOf(f.SupervisorId.ToString()) >= 0)))
: true
select f;
The idea here is to run a query in AND between two blocks of conditions 'activated' by the presence of the variables 'userLevel' and 'supervisorId'.
For example, if both userLevel and supervisoId are null the query becomes:
var qry = from f in items
where true && true
select f;
Well, this query works well only if I enclose in additional parentheses the 2 trigraphs:
var qry = from f in items
where (userLevel.HasValue
? (f.IsMinLevelIdNull() || (levelsIds.Contains(f.MinLevelId)))
: true)
&& (supervisorId.HasValue
? (f.IsSupervisorIdNull() || (!f.ChildrenVisibility && (f.SupervisorId == supervisorId.Value))
|| (f.ChildrenVisibility && (hierarchyPath.IndexOf(f.SupervisorId.ToString()) >= 0)))
: true)
select f;
The question is: why the extra parenthesis are required? My opinion is that there is a problem in the linq parser.
From 7.2.1 Operator precedence and associativity && gets evaluated before ? :
The additional parentheses would be required as the precedence is evaluated in the wrong order for example your code would be evaluated as the following because there is no break between true && supervisorId.HasValue...
var qry = from f in items
where
1st: userLevel.HasValue
?
2nd: (f.IsMinLevelIdNull() || (levelsIds.Contains(f.MinLevelId)))
:
3rd: true && supervisorId.HasValue
? (f.IsSupervisorIdNull() || (!f.ChildrenVisibility && (f.SupervisorId == supervisorId.Value))
|| (f.ChildrenVisibility && (hierarchyPath.IndexOf(f.SupervisorId.ToString()) >= 0))) **)**
: true
select f;