-
-
Notifications
You must be signed in to change notification settings - Fork 937
[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
base: develop
Are you sure you want to change the base?
Conversation
@@ -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
/// <summary> | ||
/// Sets a source stream resolver for the field. | ||
/// </summary> | ||
public virtual FieldBuilder<TSourceType, TReturnType> ResolveStream(Func<IResolveFieldContext<TSourceType>, IObservable<TReturnType?>> sourceStreamResolver) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
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. Another option would be to add So far, I just don't see how the benefits outweigh the negatives. I'll be glad to review any options you may have. |
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. |
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 We could take it even a step further, as I described above, where there is a dedicated So I would not merge yet. Certainly not as-is, with no way to use the subscription field builders. |
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. |
Another thing I'm thinking, is that the subscription field type inherits from
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. |
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. |
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
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
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
|
||
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
|
||
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
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
|
||
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
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
} | ||
|
||
/// <inheritdoc cref="IInterfaceGraphType"/> | ||
public class InterfaceGraphType : InterfaceGraphType<object> |
Check failure
Code scanning / CodeQL
Class does not implement Equals(object)
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
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:
|
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. |
Note that I think this PR is on the right track, but we should be careful to merge without a full review. |
I would not even if code is duplicated. The reason is different return types of builders to allow chaining. |
Extension methods can still do this: public static TBuilder DoSomething<TBuilder>(this TBuilder builder /* additional params */ )
where TBuilder : IFieldBuilder
{
/* do something */
return builder;
} |
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. |
I remembered why I did not use extension methods - all methods of builders are virtual. |
Codecov Report
📣 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
... 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. |
Rel: #3574 and #1176.
This is my attempt to solve the problem of excessive
StreamResolver
property forFieldType
class and API around it.TODO:
ResolveStream
method.