Skip to content

[WIP] Strong typed API for building fields #3583

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open

Conversation

sungam3r
Copy link
Member

@sungam3r sungam3r commented Apr 7, 2023

Rel: #3574 and #1176.

This is my attempt to solve the problem of excessive StreamResolver property for FieldType class and API around it.

TODO:

  • Add APIs to use ResolveStream method.

@sungam3r sungam3r added the BREAKING Breaking changes in either public API or runtime behavior label Apr 7, 2023
@sungam3r sungam3r added this to the 8.0 milestone Apr 7, 2023
@sungam3r sungam3r requested a review from Shane32 April 7, 2023 08:30
@github-actions github-actions bot added the test Pull request that adds new or changes existing tests label Apr 7, 2023
@@ -64,8 +64,8 @@

ValidateFieldArgumentsUniqueness(field, type);

if (field.StreamResolver != null && type != schema.Subscription)
throw new InvalidOperationException($"The field '{field.Name}' of an Object type '{type.Name}' must not have StreamResolver set. You should set StreamResolver only for the root fields of subscriptions.");
if (field is SubscriptionRootFieldType && type != schema.Subscription)

Check warning

Code scanning / CodeQL

Reference equality test on System.Object

Reference equality for System.Object comparisons ([this](1) argument has type IObjectGraphType).
/// <summary>
/// Sets a source stream resolver for the field.
/// </summary>
public virtual FieldBuilder<TSourceType, TReturnType> ResolveStream(Func<IResolveFieldContext<TSourceType>, IObservable<TReturnType?>> sourceStreamResolver)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both ResolveStream and ResolveStreamAsync are unused now.

throw new InvalidOperationException($"Stream resolver not set for field '{node.Field.Name}'.");
}
var resolver = (node.FieldDefinition as SubscriptionRootFieldType)?.StreamResolver
?? throw new InvalidOperationException($"Stream resolver not set for field '{node.Field.Name}'."); // TODO: this should be caught by schema validation
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll post PR into master to fix this.

/// <summary>
/// Creates a field builder used by SubscriptionField() methods.
/// </summary>
protected virtual SubscriptionRootFieldBuilder<TSourceType, TReturnType> CreateSubscriptionRootBuilder<TReturnType>([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both new methods here are unused.

@sungam3r sungam3r self-assigned this Apr 7, 2023
@Shane32
Copy link
Member

Shane32 commented Apr 9, 2023

I took a look at this today. So far I don't see this as a good solution if it requires people to use AddField rather than the field builders. This undermines much of our recent design changes, which encourage use of field builders and allows type inference and extension method use.

Now if there was a new base object type (e.g. SubscriptionObjectGraphType<T>) and the Field<T> methods on it all returned subscription field builders, that could be considered. But I fear that it will be a massive amount of duplicated code.

Another option would be to add SubscriptionField<T> methods that return a subscription field builder, but that doesn't seem to be much different than what we have now.

So far, I just don't see how the benefits outweigh the negatives. I'll be glad to review any options you may have.

@Shane32
Copy link
Member

Shane32 commented Apr 9, 2023

It's also possible that I misunderstand your changes from my very-brief scan of the changes. I would like to see a sample of what changes are required for a typical subscription schema, both with auto registering graph types, and code-first. I'm not specifically seeing it yet.

@Shane32
Copy link
Member

Shane32 commented Apr 9, 2023

It seems that you wrote another field builder, but unless I'm wrong, there's no way to use it. Let's assume you fix that issue. It seems like a lot of extra code and trouble for very little gain. In order to be worth it, this PR should prevent users from creating subscription fields at compile time.

For example, let's say you add SubscriptionField which creates a subscription field builder. There's nothing to prevent a user from calling Field rather than SubscriptionField. So we are back exactly to where we are now. The only difference is that the user has to discern between Field and SubscriptionField versus Resolve and ResolveStream. Given the two, I prefer the latter. Intellisense will show ResolveStream as an autocompletion when Resolve is beginning to be typed, whereas this is not the case with Field. Of course we could name it FieldSubscription but in the end, the user still needs to choose the right method.

We could take it even a step further, as I described above, where there is a dedicated SubsciptionGraphType. Then we could set Schema.Subscription to only allow setting a SubscriptionGraphType instance. The Field methods could be shadowed, so that the default ones are not accessible. Then we achieve compile-time checks.

So I would not merge yet. Certainly not as-is, with no way to use the subscription field builders.

@Shane32
Copy link
Member

Shane32 commented Apr 10, 2023

All my comments apply equally to changes towards the goal of having input types disallow resolvers (which I endorse): the goal is met if compile-time restrictions are in place. Otherwise we have missed the goal.

@Shane32
Copy link
Member

Shane32 commented Apr 10, 2023

Another thing I'm thinking, is that the subscription field type inherits from FieldType, so it could be assigned anywhere that FieldType is allowed. In other words, a stream resolver could be set for a non-root-subscription field. Maybe we need some type of inheritance:

  • FieldType - abstract
    • ObjectFieldType - for output object types (adds FieldResolver)
    • InterfaceFieldType - for interface object types
    • InputFieldType - for input types
    • SubscriptionFieldType - for root subscription types (adds StreamResolver)

However, these changes might be drastic to implement. I think we should sit back when complete with whatever changes we come up with and consider: are the changes worth it? Probably, I think, but let's not presuppose that the answer is yes.

@sungam3r
Copy link
Member Author

However, these changes might be drastic to implement.

I needed to start with something and I took the first step. Inheritance hierarchy looks like the second. I understand that in the end it may not work or will be too complex to programm. Nevertheless, after adding the latest changes to schema validator, I got the impression that eventually we can get a working solution.

@Shane32
Copy link
Member

Shane32 commented Apr 10, 2023

However, these changes might be drastic to implement.

I needed to start with something and I took the first step. Inheritance hierarchy looks like the second. I understand that in the end it may not work or will be too complex to programm. Nevertheless, after adding the latest changes to schema validator, I got the impression that eventually we can get a working solution.

👍

Please ping me when this PR is ready for further review.

Comment on lines +238 to +243
catch
{
throw new ArgumentException(
$"Cannot infer a Field name from the expression: '{expression.Body}' " +
$"on parent GraphQL type: '{Name ?? GetType().Name}'.");
}

Check notice

Code scanning / CodeQL

Generic catch clause

Generic catch clause.
Comment on lines +42 to +47
if (!fieldType.ResolvedType.IsGraphQLTypeReference())
{
if (fieldType.ResolvedType != null ? fieldType.ResolvedType.IsOutputType() == false : fieldType.Type?.IsOutputType() == false)
throw new ArgumentOutOfRangeException(nameof(fieldType),
$"Interface type '{Name ?? GetType().GetFriendlyName()}' can have fields only of output types: ScalarGraphType, ObjectGraphType, InterfaceGraphType, UnionGraphType or EnumerationGraphType. Field '{fieldType.Name}' has an input type.");
}

Check notice

Code scanning / CodeQL

Nested 'if' statements can be combined

These 'if' statements can be combined.
Comment on lines +333 to +338
catch
{
throw new ArgumentException(
$"Cannot infer a Field name from the expression: '{expression.Body}' " +
$"on parent GraphQL type: '{Name ?? GetType().Name}'.");
}

Check notice

Code scanning / CodeQL

Generic catch clause

Generic catch clause.

if (!fieldType.ResolvedType.IsGraphQLTypeReference())
{
if (fieldType.ResolvedType != null ? fieldType.ResolvedType.IsInputType() == false : fieldType.Type?.IsInputType() == false)

Check notice

Code scanning / CodeQL

Unnecessarily complex Boolean expression

The expression 'A == false' can be simplified to '!A'.

if (!fieldType.ResolvedType.IsGraphQLTypeReference())
{
if (fieldType.ResolvedType != null ? fieldType.ResolvedType.IsOutputType() == false : fieldType.Type?.IsOutputType() == false)

Check notice

Code scanning / CodeQL

Unnecessarily complex Boolean expression

The expression 'A == false' can be simplified to '!A'.
Comment on lines +59 to +64
if (!fieldType.ResolvedType.IsGraphQLTypeReference())
{
if (fieldType.ResolvedType != null ? fieldType.ResolvedType.IsOutputType() == false : fieldType.Type?.IsOutputType() == false)
throw new ArgumentOutOfRangeException(nameof(fieldType),
$"Object type '{Name ?? GetType().GetFriendlyName()}' can have fields only of output types: ScalarGraphType, ObjectGraphType, InterfaceGraphType, UnionGraphType or EnumerationGraphType. Field '{fieldType.Name}' has an input type.");
}

Check notice

Code scanning / CodeQL

Nested 'if' statements can be combined

These 'if' statements can be combined.

if (!fieldType.ResolvedType.IsGraphQLTypeReference())
{
if (fieldType.ResolvedType != null ? fieldType.ResolvedType.IsOutputType() == false : fieldType.Type?.IsOutputType() == false)

Check notice

Code scanning / CodeQL

Unnecessarily complex Boolean expression

The expression 'A == false' can be simplified to '!A'.
Comment on lines +65 to +70
if (!fieldType.ResolvedType.IsGraphQLTypeReference())
{
if (fieldType.ResolvedType != null ? fieldType.ResolvedType.IsInputType() == false : fieldType.Type?.IsInputType() == false)
throw new ArgumentOutOfRangeException(nameof(fieldType),
$"Input Object '{Name ?? GetType().GetFriendlyName()}' can have fields only of input types: ScalarGraphType, EnumerationGraphType or IInputObjectGraphType. Field '{fieldType.Name}' has an output type.");
}

Check notice

Code scanning / CodeQL

Nested 'if' statements can be combined

These 'if' statements can be combined.
}

/// <inheritdoc cref="IInterfaceGraphType"/>
public class InterfaceGraphType : InterfaceGraphType<object>

Check failure

Code scanning / CodeQL

Class does not implement Equals(object)

Class 'InterfaceGraphType' does not implement Equals(object), but ['Equals' is called on an instance of this class](1).
Comment on lines +294 to +299
catch
{
throw new ArgumentException(
$"Cannot infer a Field name from the expression: '{expression.Body}' " +
$"on parent GraphQL type: '{Name ?? GetType().Name}'.");
}

Check notice

Code scanning / CodeQL

Generic catch clause

Generic catch clause.
@sungam3r
Copy link
Member Author

sungam3r commented Apr 13, 2023

It is ready for further review. I added typed fields and new builders. In fact, the changes are not as dramatic as I thought.

TODOs/questions and pain points:

  1. Connection builders. See commented code block in InterfaceGraphType.cs. Now ConnectionBuilder works only with ObjectFieldType but I think it should also support InterfaceFieldType. Also I see // TODO: Remove in v5 note in code.
  2. Jumping around generics in TypeFields<T>/ITypeFields. Each derived field type has its own typed Fields property but also there is some code working with IComplexGraphType. See ComplexGraphTypeExtensions.Fields extension method, .Fields().AsEnumerable() usages and note about CS0695 error in TypeFields.cs.
  3. Some aux methods have appeared like internal static QueryArguments? Arguments(this FieldType fieldType) => fieldType is IFieldTypeWithArguments ftwa ? ftwa.Arguments : null;
  4. IComplexGraphType interface became empty. ComplexGraphType<TSourceType> class contains only ctor.
  5. ObjectGraphType.CreateSubscriptionRootBuilder is still unused. I'm not sure how to be with SubscriptionRootFieldType - inherit it from ObjectFieldType or from FieldType. I tend to the former.
  6. I do not like IFieldTypeWithArguments. Maybe change to FieldTypeWithArguments : FieldType? Not much better though.

@sungam3r sungam3r changed the title [WIP] Move StreamResolver into SubscriptionRootFieldType [WIP] Strong typed API for building fields Apr 13, 2023
@Shane32
Copy link
Member

Shane32 commented Apr 14, 2023

I need time to review. Looking over only the API changes, I was thinking that perhaps there is an opportunity to turn some field builder methods into extension methods and perform some deduplication of code. Perhaps the field builders are merely interfaces. I have to review further.

@Shane32
Copy link
Member

Shane32 commented Apr 14, 2023

Note that I think this PR is on the right track, but we should be careful to merge without a full review.

@sungam3r
Copy link
Member Author

to turn some field builder methods into extension methods and perform some deduplication of code

I would not even if code is duplicated. The reason is different return types of builders to allow chaining.

@Shane32
Copy link
Member

Shane32 commented Apr 14, 2023

Extension methods can still do this:

public static TBuilder DoSomething<TBuilder>(this TBuilder builder /* additional params */ )
    where TBuilder : IFieldBuilder
{
    /* do something */
    return builder;
}

@sungam3r
Copy link
Member Author

Yes, they can. I have not yet evaluated what is better - to have separate builders or try to reduce the code to one class through extension methods.

@sungam3r
Copy link
Member Author

I remembered why I did not use extension methods - all methods of builders are virtual.

@codecov-commenter
Copy link

Codecov Report

Merging #3583 (979dc3f) into develop (bc65011) will decrease coverage by 0.18%.
The diff coverage is 68.75%.

📣 This organization is not using Codecov’s GitHub App Integration. We recommend you install it so Codecov can continue to function properly for your repositories. Learn more

@@             Coverage Diff             @@
##           develop    #3583      +/-   ##
===========================================
- Coverage    83.99%   83.82%   -0.18%     
===========================================
  Files          383      390       +7     
  Lines        17066    17288     +222     
  Branches      2739     2768      +29     
===========================================
+ Hits         14334    14491     +157     
- Misses        2081     2145      +64     
- Partials       651      652       +1     
Impacted Files Coverage Δ
...phQL.DataLoader/Extensions/DataLoaderExtensions.cs 43.67% <ø> (ø)
...raphQL.MicrosoftDI/ScopedFieldBuilderExtensions.cs 100.00% <ø> (ø)
...c/GraphQL/Builders/SubscriptionRootFieldBuilder.cs 0.00% <0.00%> (ø)
src/GraphQL/Execution/ExecutionContext.cs 100.00% <ø> (ø)
src/GraphQL/Execution/Nodes/ArrayExecutionNode.cs 98.07% <ø> (ø)
src/GraphQL/Execution/Nodes/NullExecutionNode.cs 0.00% <ø> (ø)
src/GraphQL/Execution/Nodes/ObjectExecutionNode.cs 96.07% <ø> (ø)
.../Execution/Nodes/SubscriptionArrayExecutionNode.cs 100.00% <ø> (ø)
...Execution/Nodes/SubscriptionObjectExecutionNode.cs 100.00% <ø> (ø)
.../Execution/Nodes/SubscriptionValueExecutionNode.cs 100.00% <ø> (ø)
... and 65 more

... and 1 file with indirect coverage changes

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
BREAKING Breaking changes in either public API or runtime behavior test Pull request that adds new or changes existing tests
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy