0

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'"

Chris
  • 41
  • 5
  • 1
    Check this [answer](https://stackoverflow.com/a/66386142/10646316). It shows how to reuse DTO mapping, but it also works with predicates. – Svyatoslav Danyliv Jan 09 '23 at 19:29

0 Answers0