koenbeuk / EntityFrameworkCore.Projectables

Project over properties and functions in your linq queries
MIT License
260 stars 17 forks source link

Allow projected value to be saved in an entity #80

Closed hahn-kev closed 11 months ago

hahn-kev commented 11 months ago

I'm attempting to use this with a GQL server library (Hotchocolate). The way it works is it writes a select statement like this:

class User {
public string Id {get; set;}
public List<Contact> Contacts {get; set;}
int? _contactCount;
[Projectable]
public int ContactCount {
      get => Contacts.Count;
      set =>_contactCount = value;
   }
}

Users.Select(u => new User() { Id = u.Id, ContactCount= u.ContactCount })

later on when the data leaves the API the User is serialized and the values are read from the properties.

this works fine, except when I try to use a projectable since there's no setter. The select statment above won't work for those properties. This is why I worked on #79. However once that gets merged my issue still won't be fixed entirely. The Query will execute just fine, and the value from the db will be stored in the object using the setter on the property. However when the serializer reads ContactCount it won't be able to read the value that was output from the SQL as the code will just execute Contacts.Count. I would love it if I could return the value that was set in the private field instead, but still preserve ContactsCount.

The only thing I could think of is to write the getter in some way so it'll return the value stored in the field at runtime, but make the generator ignore that part of the code somehow.

  1. get => _contactCount ?? Contacts.Count and the first part is excluded from the query
  2. get => Project.Default(_contactCount, () => Contacts.Count) where Project.Default is basically a magic method that gets rewritten by the expression builder into just ContactsCount but it'll return the value from _contactCount by default.
  3. get => Project.Default(_contactCount, u => u.Contacts.Count) would work without performance issues because it could be a static method without a capture, but it could be really confusing, and users could write it incorrectly

The first one is pretty clean, but it would be hard to write the parser side as sometimes you might actually want that whole thing to be the SQL. It might also be confusing for anyone to read the actual code. The second option could be easier to understand and write the parser for, but it might actually have really bad performance as a new lambda would have to be created each time the getter was called. We can't just pass the value in directly as Contacts.Count could throw if Contacts is null (or any other case also).

One other option I could think of is to declare a normal property and declare the expression somewhere else. Like this

[Projectable(nameof(CalculateContactCount))]
public int ContactCount {get; set;}
private int CalculateContactCount() => Contacts.Count;

This would solve any of the weird issues seen above and might be the best option, but I'm not sure how much work it would be to support this, maybe none, I'm not sure.

I know this is a pretty out there use case but I'd like to figure something out so I can use this together with GQL.

koenbeuk commented 11 months ago

One other option I could think of is to declare a normal property and declare the expression somewhere else. Like this

This is actually already (partly) supported through the use of UseMemberBody. Though we have no tests in place to see if it workers for properties with getters and setters.

I'll take a closer look at this issue and the associated PR later.

hahn-kev commented 11 months ago

Thanks UseMemberBody works perfectly for my use case. Here's my model:

    [Projectable(UseMemberBody = nameof(InternalUserCount))]
    public int UserCount { get; set; }

    private static Expression<Func<Project, int>> InternalUserCount => project => project.Users.Count;

When I select UserCount it does exactly what I expect.

A comment on how you declare the private expression, it seems odd that we need to do this:

    private static Expression<Func<Project, int>> InternalUserCount => project => project.Users.Count;

instead of just assigning the field a value like this:

    private static Expression<Func<Project, int>> InternalUserCount = project => project.Users.Count;

notice there's only 1 =>, it's a minor thing but it could also be confusing if you did the wrong thing on accident.

koenbeuk commented 11 months ago

Comment noted, and I agree that you should be able to use the body of either a property or a field, feel free to open up a new issue of you feel that's something that should be fixed.