1

Setup

I am using a linq-to-sql query (using Linq2db) to perform a search.

Depending on which part of the application the user runs this search from, the result will have certain additional fields populated. The result object thus contains some properties that are common between parts, and some that are unique.

class UserSearchResult
{
    // always populated
    public int    UserId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Supervisor { get; set; }

    // only populated in user maintenance
    public string RoleName { get; set; }

    // only populated in user scheduling
    public int    PTOAccrued { get; set; }
    public int    ScheduleId { get; set; }
    public string ScheduleName { get; set; }
}

The columns that are specific to one part of the application are expensive to query. As such, I am currently a different query for each part:

// query for maintenance area
IQueryable<UserSearchResult> UserMaintenanceResultQry(IQueryable<User> filteredUsers) =>
    from user in filteredUsers
    join utor in db.UserToRole on user.UserId equals utor.UserId
    select new UserSearchResult()
    {
        UserId = user.UserId,
        FirstName = user.FirstName,
        LastName = user.LastName,
        Supervisor = $"{user.Supervisor.FirstName} {user.Supervisor.LastName} ({user.SupervisorId})",

        RoleName = utor.Role.RoleName // uses navigation property
    };

// query for scheduling area
IQueryable<UserSearchResult> UserSchedulingResultQry(IQueryable<User> filteredUsers) =>
    from user in filteredUsers
    join utos in db.UserToSchedule on user.UserId equals utos.UserId
    join pto  in db.PTO on user.UserId equals pto.UserId
    select new UserSearchResult()
    {
        UserId = user.UserId,
        FirstName = user.FirstName,
        LastName = user.LastName,
        Supervisor = $"{user.Supervisor.FirstName} {user.Supervisor.LastName} ({user.SupervisorId})",

        PTOAccured = pto.PTOAccrued,
        ScheduleId = utos.ScheduleId,
        ScheduleName = utos.Schedule.ScheduleName
    };

This is, of course, example code. There are lots of properties (both common and unique) and the joins are often complex and expensive.

My Question

In my real code, there are about 20 common properties and 3 areas. That means I duplicate the exact same common property-setting code in 3 different places. I would like a way to eliminate this duplication.

What I've Tried

One method that actually works is to create a constructor for UserSearchResult:

// constructor
public UserSearchResult(User user)
{
    UserId = user.UserId;
    FirstName = user.FirstName;
    // ...etc
}

// and then in the query ...
    from user in filteredUsers
    join utor in db.UserToRole on user.UserId equals utor.UserId
    select new UserSearchResult(user)
    {
        // perfect! Now I can just set the unique properties!
        RoleName = utor.Role.RoleName // uses navigation property
    };

There are two major problems with this approach:

  1. Using the User object as a constructor parameter causes every field from the User table to be queried, even if it's not used. This query often 10s of thousands of results, so pulling (a lot of) additional columns isn't acceptable.
  2. Navigation properties cannot be used within the constructor, so the Supervisor field cannot be set in the constructor. I COULD use linq2db's LoadWith method to load the navigation objects, but that in turn would load the entire navigation object instead of just the properties needed.

Conclusion

Is there any way to compose my queries to avoid needing to repeat the common properties that need to be set?

Daniel
  • 1,695
  • 15
  • 33
  • We can easily do `query.Select(u => new User {...}).Enrich(u => new User {...})` if it is acceptable for you. Not so elegant as can be. – Svyatoslav Danyliv Apr 12 '22 at 18:36
  • @SvyatoslavDanyliv Wouldn't this require setting every single property again? I'm not familiar with `Enrich` so I'm assuming you're just suggesting creating an entirely new User entity, but let me know if there's a library you're using or something. – Daniel Apr 13 '22 at 13:18
  • `Enrich` is possible function implementation. – Svyatoslav Danyliv Apr 13 '22 at 13:22
  • @SvyatoslavDanyliv Okay. Then it would require setting all the properties again which would defeat the purpose; I'd still end up repeating all those property assignments. – Daniel Apr 13 '22 at 15:34
  • Nope, in `Enrich` you can specify overridden properties. Better for sure if it will be supported by linq2db itself, but not now. Will prepare code. – Svyatoslav Danyliv Apr 13 '22 at 15:45
  • @SvyatoslavDanyliv So `Enrich` is like an implementation of the new `with` syntax for records? – NetMage Apr 13 '22 at 18:42
  • @NetMage, something like that. – Svyatoslav Danyliv Apr 13 '22 at 19:31
  • 1
    @SvyatoslavDanyliv `ExpressionVisitor` that modifies `MemberInitExpression.Bindings` by merging the two lambdas :) – NetMage Apr 13 '22 at 20:23
  • @SvyatoslavDanyliv Unfortunately I don't see a way to implement `Enrich` without having to manually pass the base `T` from the `query` since after the `Select` the type is e.g. `IQueryable` and you no longer have static access to `User`. – NetMage Apr 13 '22 at 21:09
  • 1
    @Daniel, I have created some prototype, it works in simple cases, but not with `ExpressionMethod`, so better to copy paste code at this time. Will take into account for the next major version release to support such extension. – Svyatoslav Danyliv Apr 14 '22 at 09:39
  • @SvyatoslavDanyliv If your protoype is on a branch on github, would you mind sharing a link? I'd love to see what you did and whether I could adapt it into something that would work temporarily for my use case. – Daniel Apr 14 '22 at 18:40

0 Answers0