2

It seems that I need some help from EF guru. So, I have Postgres DB with EF Core in my project. There is a table RequestDbModel which might contain many rows with the same Id and different ts. I need to select Request collection from this table where Items are rows with the same Id and also some extra fields have to be filled from these Items ordered by ts. I can do this, but only if I am materializing the query. I believe this can be done without materializing, but I cannot figure out how and I would appreciate any help

  • Request.Status - Status of the lates (newest) item in Items
  • Request.CreatedTime - ts of the first (oldest) item in Items
  • Request.AppliedTime - oldest ts of item in Items with Status == "Approved"
  • Request.CreatorId - UserId of the first (oldest) item in Items
  • Request.ApproverId - UserId for the oldest item in Items with Status == "Approved" or null
  • Request.Comment - Comment for the oldest item in Items
// builder.HasKey(p => new { p.Id, p.Timestamp })
//         .HasName("requests_pkey");
public class RequestDbModel
{
    public long Id { get; init; }
    public required string Status { get; set; }
    public required DateTime Timestamp { get; init; }
    public required Guid UserId { get; init; } 
    public string? Comment { get; init; }
}

public sealed class Request
{
    public required long Id { get; init; }
    public required string Status { get; init; }
    public DateTime CreatedTime { get; init; }
    public DateTime? AppliedTime { get; init; }
    public required Guid CreatorId { get; init; }
    public Guid? ApproverId { get; init; }
    public string? Comment { get; init; }
    public required RequestItemHistory[] Items { get; init; }
}

public sealed class RequestItemHistory
{
    public required string Status { get; init; }
    public required DateTime Timestamp { get; init; }
    public required Guid UserId  { get; init; }
    public string? Comment { get; init; }
}
public IQueryable<Request> GetRequests()
{
    var groups = context.Requests.AsNoTracking()
        .GroupBy(r => r.Id)
        .Select(g => new
        {
            Id = g.Key,
            Items = g.OrderBy(r => r.Timestamp).Select(r => new RequestItemHistory
            {
                Status = r.Status,
                Timestamp = r.Timestamp,
                UserId = r.UserId,
                Comment = r.Comment,
            })//.ToList()
        });

    var result = groups
        .ToList()
        .Select(x => new Request
        {
            Id = x.Id,
            Status = x.Items.Last().Status,
            CreatedTime = x.Items.First().Timestamp,
            AppliedTime = x.Items.Where(i => i.Status == "Approved").OrderByDescending(i => i.Timestamp).Select(i => (DateTime?)i.Timestamp).FirstOrDefault(),
            CreatorId = x.Items.First().User,
            ApproverId = x.Items.Where(i => i.Status == "Approved").OrderByDescending(i => i.Timestamp).Select(i => i.User).FirstOrDefault(),
            Comment = x.Items.Last().Comment,
            Items = x.Items.ToArray()
        }).AsQueryable();

    return result;        
}

So, I have re-writed my code and it now works without using .ToList()/.ToArray(), but I still do not understand will this code executed on server side or on client side. How to figure out this? Is this solution better than the original one?

public sealed class Request
{
    public required long Id { get; init; }
    public required string Status { get; init; }
    public DateTime CreatedTime { get; init; }
    public DateTime? AppliedTime { get; init; }
    public required Guid CreatorId { get; init; }
    public Guid? ApproverId { get; init; }
    public string? Comment { get; init; }
    public required IEnumerable<RequestItemHistory> Items { get; init; }
}

public IQueryable<Request> GetRequests()
{
    var groups = context.Requests.AsNoTracking()
        .GroupBy(r => r.Id)
        .Select(g => new
        {
            Id = g.Key,
            Status = g.OrderByDescending(x => x.Timestamp).Select(x => x.Status).First(),
            CreatedTime = g.OrderBy(x => x.Timestamp).Select(x => x.Timestamp).First(),
            AppliedTime = g.Where(x => x.Status == RequestStatusConstants.Approved).OrderByDescending(x => x.Timestamp).Select(x => (DateTime?)x.Timestamp).FirstOrDefault(),
            CreatorId = g.OrderBy(x => x.Timestamp).Select(x => x.CreatorId).First(),
            ApproverId = g.Where(x => x.Status == RequestStatusConstants.Approved).OrderByDescending(x => x.Timestamp).Select(x => (Guid?)x.ApproverId).FirstOrDefault(),
            Comment = g.OrderByDescending(x => x.Timestamp).Select(x => x.Comment).First(),
            Items = g.OrderBy(r => r.Timestamp).Select(r => new RequestItemHistory
            {
                Status = r.Status,
                Timestamp = r.Timestamp,
                UserId = r.UserId,
                Comment = r.Comment,
            })
        });

    var result = groups
        .Select(x => new Request
        {
            Id = x.Id,
            Status = x..Status,
            CreatedTime = x.CreatedTime,
            AppliedTime = x.AppliedTime,
            CreatorId = x.CreatorId,
            ApproverId = x.ApproverId,
            Comment = x.Comment,
            Items = x.Items
        });

    return result;        
}
4
  • 2
    groups.ToList() why is the ToList() there? Also do you actually need the whole Items list, because if so you are forced to get all of it, and therefore might be better doing the other aggregations client-side. Commented Jul 8 at 9:15
  • @Charlieface If I do not use groups.ToList() I get an exception The LINQ expression 's => s.Status' 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'.
    – wdtv
    Commented Jul 8 at 9:25
  • 2
    There are a couple of problems with what it seems you want to accomplish. Firstly, there is no point in your method returning IQueryable<T> when you are projecting and materializing a result. Something like that will pretty much need to involve a double-projection. You can optimize it to avoid extra enumerations between the first/last etc. and have the DB return those which might perform a bit better but the fact you're returning all items anyways the resource footprint is essentially the same.
    – Steve Py
    Commented Jul 8 at 10:07
  • 1
    I see that you are retrieveing ALL requests from Database. Do grouping on the client side, it will be more performant. Commented Jul 9 at 9:24

0

Browse other questions tagged or ask your own question.