graphql-platform icon indicating copy to clipboard operation
graphql-platform copied to clipboard

Support fetching dependent navigation properties when computed field depends on another field

Open francisrath opened this issue 3 years ago • 11 comments

Is your feature request related to a problem? Please describe. I have a model where one field is a list of licenses for a customer, and there is a computed field for "hasValidLicense". This requires certain fields to be included in the query for the calculation to work.

Describe the solution you'd like I'd like to mark the dependancies of a field with a resolver, so that selecting the resolved field causes the required fields to be fetched from the data source, even if the GraphQL query does not ask for it.

Describe alternatives you've considered Tried to add .IsProjected(false) to the field with the resolver, but it doesn't trigger fetching the required underlying data unless it is included in the GraphQL query

Additional context The very best solution, at least as an end used, would be that the library detects which fields are required and fetches them automatically. The most realistic while still fetching the minimal set of data is hopefully something like this:

descriptor.Field(f => f.AccessTokens)
    .Authorize(Policy.MotiviewBackofficeClient)
    .Name("licenses");

descriptor.Field("hasValidLicense")
    .DependsOn(f => f.AccessTokens, at => new { at.ValidFrom, at.ValidTo })
    .Authorize(Policy.MotiviewBackofficeClient)
    .Resolver(ctx => ctx.Parent<Customer>()
        .AccessTokens.Any(t =>
        (t.ValidFrom <= DateTime.UtcNow) &&
        (t.ValidTo == null || t.ValidTo > DateTime.UtcNow)
    ));

My idea is to mark which fields the resolved property requires, and then optionally specify which fields that should be fetched from each if it is a collection.

For example:

descriptor.Field("hasLongName")
    .DependsOn(f => f.Name)
    .Resolver(ctx => ctx.Parent<Customer>().Name.Length > 50);

descriptor.Field("hasCustomersWithLongNames")
    .DependsOn(f => f.Customers, c =>c.Name)
    .Resolver(ctx => ctx.Parent<Customer>().Customers.Any(c=>c.Name.Length > 50));

descriptor.Field("averageBodyMassIndex")
    .DependsOn(f => f.FamilyMembers, fm =>new { fm.Height, fm.Weight })
    .Resolver(ctx => ctx.Parent<Person>().FamilyMembers.Average(p=>p.Weigth/(p.Height*p.Height)));

Edit: It should also allow depending on fields that are not exposed in GraphQL, for example like this:

descriptor.Field("coordinate")
    .IsProjected(false)
    .Resolve(ctx => new Coordinate(ctx.Parent<Customer>().Longitude, ctx.Parent<Customer>().Latitude));

Currently, I have to add Longitude and Latitude fields to the GraphQL API to make this work

francisrath avatar Nov 14 '20 13:11 francisrath

Ideally this could also allow controlling authorization per field as well, so that the computed field can be public, even if the list is protected (hasValidLicense vs licenses in the first code snippet)

francisrath avatar Nov 14 '20 13:11 francisrath

Thanks for writing this down.

We also have thought on this API a lot, we, however, moved work on this to a later release 11.x

I will add this to the backlog.

michaelstaib avatar Nov 14 '20 16:11 michaelstaib

@francisrath I will add a PR so IsProjected works with nested objects too, so there will be a workaround for this ResolverMetadata will come later.

PascalSenn avatar Nov 14 '20 16:11 PascalSenn

@michaelstaib is this something we can contribute and hopefully get in 11.0 or will it definitively be 11.x?

My colleague @vegardlarsen might be able to this if he can have some pointers on how to get started :)

francisrath avatar Nov 17 '20 15:11 francisrath

11.0 is feature complete ... we are only doing bug-fixes. But if you like to contribute to 11.1 we are all in. We are creating the final RC build on Friday. 11.1 previews will start end of next week and the branch is called develop, so if you guys want to contribute we can talk at the end of next week.

michaelstaib avatar Nov 17 '20 23:11 michaelstaib

@michaelstaib Can you please reopen this?

glen-84 avatar Jul 03 '22 08:07 glen-84

@PascalSenn is that something that is already solved with v13 projections?

michaelstaib avatar Jul 03 '22 09:07 michaelstaib

@michaelstaib no this has still to be implemented

PascalSenn avatar Jul 03 '22 09:07 PascalSenn

Maybe Projects, invokable multiple times?

descriptor.Field("fullName")
    .Projects(u => u.FirstName)
    .Projects(u => u.LastName)
    .Resolve(ctx => ctx.Parent<User>().FirstName + " " + ctx.Parent<User>().LastName);

Could also be written as (based on the OP):

descriptor.Field("fullName")
    .Projects(u => new { u.FirstName, u.LastName })
    .Resolve(ctx => ctx.Parent<User>().FirstName + " " + ctx.Parent<User>().LastName);

Maybe there could be an overload of Resolve that gives you the parent automatically?:

descriptor.Field("fullName")
    .Projects(u => new { u.FirstName, u.LastName })
    .Resolve(u => $"{u.FirstName} {u.LastName}");

glen-84 avatar Sep 13 '22 07:09 glen-84

Hey, as a workaround, you can try out Projectables, it did the trick for me https://github.com/koenbeuk/EntityFrameworkCore.Projectables/blob/master/samples/BasicSample/Program.cs

    [Projectable(UseMemberBody = nameof(_SampleComputed))]
    public string SampleComputed { get; set; }
    private string _SampleComputed => A + B

claudeclement avatar Jan 05 '24 18:01 claudeclement

To anyone else using Projectables: I had to add [NotMapped] to the Projectable property to prevent "column does not exist" errors when inserting the entity with the [Projectable] property.

Tommsy64 avatar Jan 30 '24 01:01 Tommsy64