Jesus
Jesus

Reputation: 475

Linq code improvement issues with filters

I have this linq query:

private async Task<IQueryable<MyModel>> GetClaimList(MyDto input)
{
    var claimQry = await _claimRepository.WithDetailsAsync();
    var claimBatchQry = await _claimBatchRepository.GetQueryableAsync();
    var remittanceClaimQry = await _remittanceClaimRepository.WithDetailsAsync();

    var claims = (from claimBatch in claimBatchQry
                from transactionSet in claimBatch.TransactionSets.DefaultIfEmpty()
                from transactionSetClaim in transactionSet.Claims.DefaultIfEmpty()
                join claim in claimQry on transactionSetClaim.ClaimId equals claim.Id
                where ...
                orderby claimBatch.Id descending
                group new { claimBatch, transactionSet, claim } by claim.Id into grouped
                select new ClaimTransactionSetBatch
                {
                    Batch = grouped.Select(x => x.claimBatch).FirstOrDefault(),
                    TransactionSet = grouped.Select(x => x.transactionSet).FirstOrDefault(),
                    Claim = grouped.Select(x => x.claim).FirstOrDefault()
                }).ToList();


    return claims.AsQueryable();
}

As you can see, I used FirstOrDefault() to retrieve the first elements from the grouped data, ensuring efficient data retrieval without unnecessary enumeration.

But I need to return it AsQueryable() because in another method I have:

public async Task<PagedResultDto<ClaimSummaryDto>> GetListAsync(GetClaimListDto input)
{
    var claimTransactionSetBatch = await this.GetClaimList(input);

    var claims = claimTransactionSetBatch.Select(x => new ClaimSummaryDto()
    {
        AgencyIdentifier = x.Claim.AgencyIdentifier,
        ....
    }

    claims = claims.OrderBy(string.IsNullOrWhiteSpace(input.Sorting) ? "DateOfServiceStart" : input.Sorting).PageBy(input);

    return new PagedResultDto<ClaimSummaryDto>(total, claims.ToList());
}

So, instead return an IQueryable, if I return it as a list, this code:

claims.OrderBy(string.IsNullOrWhiteSpace(input.Sorting) ? "DateOfServiceStart" : input.Sorting).PageBy(input);

Throws an error, because it need to be an IQueryable to perform the sorting:

Severity Code Description Project File Line Suppression State Error CS1929 'IEnumerable' does not contain a definition for 'OrderBy' and the best extension method overload 'DynamicQueryableExtensions.OrderBy(IQueryable, string, params object?[])' requires a receiver of type 'System.Linq.IQueryable'

So, I'm worried about the performance issues of this query because we materialize the query when we use .ToList(), then move it to AsQueryable again and in the method where is used, we convert it to a list again, how can I improve this code? Also, if I do not apply the .ToList()from the GetClaimList method like this:

...

    {
        Batch = grouped.Select(x => x.claimBatch).FirstOrDefault(),
        TransactionSet = grouped.Select(x => x.transactionSet).FirstOrDefault(),
        Claim = grouped.Select(x => x.claim).FirstOrDefault()
    });


return claims;

It throws a big error when it tries to materialize the query at the end:

return new PagedResultDto<ClaimSummaryDto>(total, claims.ToList());

Error:

The LINQ expression 'DbSet() .Where(c => __ef_filter__p_0 || (Guid?)EF.Property(c, "TenantId") == __ef_filter__CurrentTenantId_1) .SelectMany( collectionSelector: c => DbSet() .Where(c0 => EF.Property<Guid?>(c, "Id") != null && object.Equals( objA: (object)EF.Property<Guid?>(c, "Id"), objB: (object)EF.Property<Guid?>(c0, "ClaimBatchId"))) .DefaultIfEmpty(), resultSelector: (c, c) => new TransparentIdentifier<ClaimBatch, ClaimTransactionSet>( Outer = c, Inner = c )) .SelectMany( collectionSelector: ti => DbSet() .Where(c1 => EF.Property<Guid?>(ti.Inner, "Id") != null && object.Equals( objA: (object)EF.Property<Guid?>(ti.Inner, "Id"), objB: (object)EF.Property<Guid?>(c1, "TransactionSetId"))) .DefaultIfEmpty(), resultSelector: (ti, c) => new TransparentIdentifier<TransparentIdentifier<ClaimBatch, ClaimTransactionSet>, ClaimTransactionSetClaim>( Outer = ti, Inner = c )) .Join( inner: DbSet() .Where(c2 => __ef_filter__p_0 || (Guid?)EF.Property(c2, "TenantId") == __ef_filter__CurrentTenantId_1), outerKeySelector: ti0 => (object)ti0.Inner.ClaimId, innerKeySelector: c2 => (object)c2.Id, resultSelector: (ti0, c2) => new TransparentIdentifier<TransparentIdentifier<TransparentIdentifier<ClaimBatch, ClaimTransactionSet>, ClaimTransactionSetClaim>, Claim>( Outer = ti0, Inner = c2 )) .Where(ti1 => ti1.Outer.Outer.Outer.SourceClaimBatchId == null && True && True && False || __input_Status_1 .Contains(ti1.Inner.Status) && True && True && True && True) .OrderByDescending(ti1 => ti1.Outer.Outer.Outer.Id) .GroupBy(ti1 => ti1.Inner.Id) .Select(g => new ClaimTransactionSetBatch{ Batch = g .AsQueryable() .Select(e0 => e0.Outer.Outer.Outer) .FirstOrDefault(), TransactionSet = g .AsQueryable() .Select(e0 => e0.Outer.Outer.Inner) .FirstOrDefault(), Claim = g .AsQueryable() .Select(e0 => IncludeExpression( EntityExpression: IncludeExpression( EntityExpression: IncludeExpression( EntityExpression: IncludeExpression( EntityExpression: e0.Inner, NavigationExpression: MaterializeCollectionNavigation( Navigation: Claim.ServiceLines, subquery: DbSet() .Where(c3 => EF.Property<Guid?>(e0.Inner, "Id") != null && object.Equals( objA: (object)EF.Property<Guid?>(e0.Inner, "Id"), objB: (object)EF.Property<Guid?>(c3, "ClaimId")))), ServiceLines) , NavigationExpression: MaterializeCollectionNavigation( Navigation: Claim.CustomProperties, subquery: DbSet() .Where(c4 => EF.Property<Guid?>(e0.Inner, "Id") != null && object.Equals( objA: (object)EF.Property<Guid?>(e0.Inner, "Id"), objB: (object)EF.Property<Guid?>(c4, "ClaimId")))), CustomProperties) , NavigationExpression: MaterializeCollectionNavigation( Navigation: Claim.TransactionSets, subquery: DbSet() .Where(c5 => EF.Property<Guid?>(e0.Inner, "Id") != null && object.Equals( objA: (object)EF.Property<Guid?>(e0.Inner, "Id"), objB: (object)EF.Property<Guid?>(c5, "ClaimId")))), TransactionSets) , NavigationExpression: MaterializeCollectionNavigation( Navigation: Claim.EventHistory, subquery: DbSet() .Where(c6 => __ef_filter__p_0 || (Guid?)EF.Property(c6, "TenantId") == __ef_filter__CurrentTenantId_1) .Where(c6 => EF.Property<Guid?>(e0.Inner, "Id") != null && object.Equals( objA: (object)EF.Property<Guid?>(e0.Inner, "Id"), objB: (object)EF.Property<Guid?>(c6, "ClaimId")))), EventHistory) ) .FirstOrDefault() } ) .OrderBy(e1 => e1.Claim.ServiceLines .Select(x => x.DateOfServiceStart) .Where(date => date.HasValue) .Min())' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.

And I think this is because the firstOrDefault() need to be materialized first (This is why I used the.ToList())

Classes:

ClaimTransactionSetBatch

internal class ClaimTransactionSetBatch
{
    public Claim Claim { get; set; }
    public ClaimTransactionSet TransactionSet { get; set; }
    public ClaimBatch Batch { get; set; }
}

Claim:

public class Claim : AuditedAggregateRoot<Guid>, IMultiTenant, IProcessibleType {
    private IList<ClaimEvent> _eventHistory;
    public IList<ClaimEvent> EventHistory { get => this._eventHistory.OrderByDescending(e => e.TimeStamp).ToList(); set => _eventHistory = value; }
    private string _claimTypeCode;
    private decimal? _netPaidAmount;
    private ClaimPaymentStatus _paymentStatus;
    private ClaimVisitMatchStatus _visitMatchStatus;
    private ClaimStatus _status;
    private IList<ClaimAction> _claimActions;
    public IList<ClaimAction> AvailableActions { get { ... } }
    public Guid? TenantId { get; set; }
    public string SubmitterIdentifier { get; set; }
    public string ReceiverIdentifier { get; set; }
    public ClaimPaymentStatus PaymentStatus { get => this._paymentStatus; set { ... } }
    public string ClaimIdentifier { get; set; }
    public string BatchIdentifier { get; set; }
    // Include the rest of the properties here
    public string ClientGender { get; set; }
    public string ClientDiagnosisCode { get; set; }
    public Decimal NetAmount { get; set; }
    public string ClaimTypeCode { get => this._claimTypeCode; set { ... } }
    public ClaimType ClaimType { get; private set; }
    public string FacilityTypeCode { get; set; }
    public ClaimProcessingStatus ProcessingStatus { get; set; }
    public string ProcessingResultMessage { get; set; }
    public ClaimStatus Status { get => this._status; set { ... } }
    public Guid? ProcessingRulesetId { get; set; }
    public ClaimVisitMatchStatus VisitMatchStatus { get => this._visitMatchStatus; set { ... } }
    public decimal? NetPaidAmount { get => this._netPaidAmount; set { ... } }
    public DateTime? LastPaymentReceivedDate { get; set; }
    public string CostCenterIdentifier { get; set; }
    public string CostCenterCode { get; set; }
    public IList<ClaimServiceLine> ServiceLines { get; set; }
    public IList<ClaimCustomProperty> CustomProperties { get; set; }
    public IEnumerable<ClaimTransactionSetClaim> TransactionSets { get; private set; }
    public ClaimRulesetEntityType ClaimRulesetEntityType => ClaimRulesetEntityType.Claim;
    
    public Claim() {
        this.ServiceLines = new List<ClaimServiceLine>();
        this.CustomProperties = new List<ClaimCustomProperty>();
        this.EventHistory = new List<ClaimEvent>();
        this._eventHistory = new List<ClaimEvent>();
        this._claimActions = new List<ClaimAction>();
        this._paymentStatus = ClaimPaymentStatus.PaymentPending;
        this._status = ClaimStatus.InProcess;
    }

    public Claim(Guid id) : base(id) {
        this.ServiceLines = new List<ClaimServiceLine>();
        this.CustomProperties = new List<ClaimCustomProperty>();
        this.EventHistory = new List<ClaimEvent>();
        this._eventHistory = new List<ClaimEvent>();
        this._claimActions = new List<ClaimAction>();
        this._paymentStatus = ClaimPaymentStatus.PaymentPending;
        this._status = ClaimStatus.InProcess;
    }

    public Claim(Guid id, Guid claimTransactionSetId) : this(id) {
        //ClaimTransactionSetId = claimTransactionSetId;
    }
}

ClaimTransactionSet

public class ClaimTransactionSet : Entity<Guid>
{
    private IList<ClaimTransactionSetClaim> claims = new List<ClaimTransactionSetClaim>();

    private DateTime? transmissionStatusChangedDate;
    private TransmissionStatus transmissionStatus;

    public Guid ClaimBatchId { get; set; }
    public Guid? SourceClaimTransactionSetId { get; set; }

    public IEnumerable<ClaimTransactionSetClaim> Claims => this.claims;

    public string GroupControlNumber { get; set; }
    public string GroupSenderCode { get; set; }
    public string GroupReceiverCode { get; set; }
    public string TransactionSetControlNumber { get; set; }

    public TransmissionStatus TransmissionStatus { get => this.transmissionStatus; set { if (this.transmissionStatus != value) { this.transmissionStatus = value; this.transmissionStatusChangedDate = DateTime.UtcNow; } } }

    public DateTime? TransmissionStatusChangedDate => this.transmissionStatusChangedDate;
}

ClaimBatch:

public class ClaimBatch : AuditedAggregateRoot<Guid>, IMultiTenant, IProcessibleType
{
    public Guid? TenantId { get; set; }
    public Guid? SourceClaimBatchId { get; set; }
    public string InterchangeControlNumber { get; set; }
    public string BatchIdentifier { get; set; }
    public DateTime? BatchDate { get; set; }
    public string SenderIdentifier { get; set; }
    public string ReceiverIdentifier { get; set; }
    public string AgencyIdentifier { get; set; }
    public DateTime ReceivedDate { get; set; }
    public string Filename { get; set; }
    public ClaimBatchProcessingMethod ProcessingMethod { get; set; }

    public ClaimProcessingStatus ProcessingStatus { get; set; }
    public string ProcessingResultMessage { get; set; }
    public Guid? ProcessingRulesetId { get; set; }

    public IList<ClaimTransactionSet> TransactionSets { get; set; }

    public IList<ClaimBatchEvent> EventHistory { get; set; }
}

Upvotes: 1

Views: 68

Answers (2)

Svyatoslav Danyliv
Svyatoslav Danyliv

Reputation: 27282

This query should be transletable.

var claimBatchQry = ... // initialize as IQueryable created from DbSet

var allRecords = 
    from claimBatch in claimBatchQry
    from transactionSet in claimBatch.TransactionSets
    from transactionSetClaim in transactionSet.Claims
    where ...
    select new { claimBatch, transactionSet, transactionSetClaim };

 var resultQuery = 
    from d in allRecords.Select(d => new { ClaimId = d.transactionSetClaim.ClaimId })
    from r in allRecords.Where(r => r.transactionSetClaim.ClaimId == d.ClaimId)
        .OrderByDescending(r => r.claimBatch.Id)
        .Take(1)
    select new ClaimTransactionSetBatch
    {
        Batch = r.claimBatch,
        TransactionSet = r.transactionSet,
        Claim = r.transactionSet.Claim
    };

Upvotes: 0

Charlieface
Charlieface

Reputation: 71119

Ideally you would only have one context, and then you can do it as one big IQueryable.

But if that is not possible:

You should remove the AsQueryable and keep the final result as IEnumerable.

Then in the ordering, for Enumerable.OrderBy you need an actual delegate, not a string property name.

One option is a dictionary of sorting functions

public static Dictionary<string, Func<IEnumerable<ClaimSummaryDto>, IEnumerable<ClaimSummaryDto>>> _funcs = new()
{
    { "SomeColumn", source => source.OrderBy(c => c.SomeColumn) },
    { "SomeColumn2", source => source.OrderBy(c => c.SomeColumn2) },
};

public async Task<PagedResultDto<ClaimSummaryDto>> GetListAsync(GetClaimListDto input)
{
    var claimTransactionSetBatch = await this.GetClaimList(input);

    var claims = claimTransactionSetBatch.Select(x => new ClaimSummaryDto()
    {
        AgencyIdentifier = x.Claim.AgencyIdentifier,
        ....
    };

    _funcs.TryGetValue(input.Sorting, out var func);
    func ??= source => source.OrderBy(c => c.DateOfServiceStart);

    claims = func(claims).PageBy(input);

   return new PagedResultDto<ClaimSummaryDto>(total, claims.ToList());
}

Upvotes: 0

Related Questions