Skip to content
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

Do not register unused graph types #1887

Merged
merged 8 commits into from
Oct 14, 2020
Merged

Do not register unused graph types #1887

merged 8 commits into from
Oct 14, 2020

Conversation

Shane32
Copy link
Member

@Shane32 Shane32 commented Oct 8, 2020

This PR prevents GraphTypesLookup from registering custom .Net scalar graph types (e.g. DateTimeOffsetGraphType) that are not used by the schema. See #1885

Also allows custom Schema implementations necessary hooks to edit the list of built-in scalar graph types during schema initialization. Not actually necessary to solve #1885 since the UriGraphType would never have been registered if it was not used, so there would be no conflict.

This currently targets version 3.1. Should it target 4.0? The binary is backwards compatible, but generated schemas will not include unused types, so introspection results will differ (even though the missing types are unused by the schema).

@Shane32 Shane32 added this to the 3.1 milestone Oct 8, 2020
@Shane32 Shane32 requested a review from sungam3r October 8, 2020 06:09
@Shane32 Shane32 self-assigned this Oct 8, 2020
@randyridge
Copy link
Contributor

randyridge commented Oct 8, 2020

To be explicit, what I hit was that going from 2.x to 3.x -- I had my own UriType, the base library added it's own UriType (both having name Uri/UriType, I can't recall). Scanning finds both with the same name and fires exception. In this case UriType was 'used', but it's shape differed. I'll try to file a repro of this tomorrow as well.

@sungam3r
Copy link
Member

sungam3r commented Oct 8, 2020

Relates to #1423 .

These changes should be carefully reviewed prior to merging.

@sungam3r
Copy link
Member

sungam3r commented Oct 8, 2020

I skimmed through the edits. Yet it seems to me that this issue should proceed differently. I'll be back with an answer later.

@Shane32
Copy link
Member Author

Shane32 commented Oct 8, 2020

@Shane32
Copy link
Member Author

Shane32 commented Oct 8, 2020

Faced with a similar problem in my graphql client generator, I came to the conclusion that all custom scalars should be connected externally. The base library should only provide 5 built-in scalars.

Originally posted by @sungam3r in https://github.com/graphql-dotnet/graphql-dotnet/issue_comments/705484550

That's EXACTLY what this PR does. Only the 5 basic scalars are registered with a simple schema. And just like any graph type, additional scalars referenced by the graph are automatically added to the schema. The only difference here is that we have special code to prevent the extra provided scalars (like UriGraphType) from having to be registered with the service provider.

There might be another way to go about it, but I think this is the right answer. We should not include these extra scalars in the schema by default. Nor should we remove them from the library. And there's no reason to force users to register them with their DI provider. Finally, it's not a breaking change -- in the sense that all existing code will work perfectly with this update.

@sungam3r
Copy link
Member

sungam3r commented Oct 8, 2020

We should not include these extra scalars in the schema by default. Nor should we remove them from the library.

Well, actually, I think that moving all custom scalars into a separate package is the right way.

And there's no reason to force users to register them with their DI provider.

The reason is to make the library free of implementation assumptions and not create conflicts with custom scalars. Custom scalars are a good example of when you can move a piece of code into a separate package without negative consequences.

It will be breaking change for clients, yes. I think this is a long-awaited technical debt. The emergence of individual packages is a normal evolutionary process.

@Shane32
Copy link
Member Author

Shane32 commented Oct 8, 2020

I'm going to simplify this PR a bit, and get rid of the other changes...

@Shane32
Copy link
Member Author

Shane32 commented Oct 8, 2020

Let's assume for the moment that in 4.0 we have custom scalars in a separate nuget package. Is there any reason not to include this change in 3.1? I think we should.

I have removed the API changes, so please re-review.

@sungam3r
Copy link
Member

sungam3r commented Oct 8, 2020

Give me 1-2 days.

@sungam3r sungam3r linked an issue Oct 10, 2020 that may be closed by this pull request
@sungam3r
Copy link
Member

It will take me longer. I had to deal with other bugfixes.

@sungam3r
Copy link
Member

Nice workaround. This is an understandable little change. However, I would like to simplify a few things. I'll be back with another PR.

@@ -184,6 +189,27 @@ public IEnumerable<IGraphType> All()
}
}

private void AddType(Type builtInGraphType)
Copy link
Member

Choose a reason for hiding this comment

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

I would suggest perhaps a more distinct name like AddBuiltInType? Too closely named to the other AddType methods I think.

I like this direction. 👍🏼

Copy link
Member Author

Choose a reason for hiding this comment

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

Good idea! - done. 👍

Let me know if you have any other comments. It's a pretty simple enhancement, and will, I believe, work great all the way 'round.

Copy link
Member

Choose a reason for hiding this comment

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

I already intervened in this PR and changed a lot. This method will no longer exist. I will update the PR and describe the changes in the next hour.

Copy link
Member

Choose a reason for hiding this comment

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

Resolving conflicts 😞

@Shane32
Copy link
Member Author

Shane32 commented Oct 14, 2020

Nice workaround. This is an understandable little change. However, I would like to simplify a few things. I'll be back with another PR.

I copied from the AddType<T>() and AddType<T>(context) methods to create AddBuiltInType(). If you think I should eliminate duplicated code between the functions, I can add a CreateTypeCollectionContext() method.

Since these new methods and fields are all private, we can refactor at will later also.

@sungam3r
Copy link
Member

Will update PR soon.

@@ -99,7 +99,7 @@ public override bool Equals(object obj)
}

/// <summary>
/// This sucks, find a better way
/// TODO: This sucks, find a better way
Copy link
Member

Choose a reason for hiding this comment

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

I understood this remark, but I don’t know how best to proceed. Maybe later.

@sungam3r
Copy link
Member

What was done:

  1. Move introspection result into json file. I'm terribly tired of editing it in the source without the ability to highlight syntax and collapse blocks.
  2. Further removal of unused types, even built-in ones. See discussions from Remove unused scalars from SDL #1423 and Clarify note about unused custom scalars graphql/graphql-spec#648
  3. Rework of GraphTypesLookup:
    a) Move built-in graph types and introspetion types into static.
    b) Eliminate allocations of graph types in Activator.CreateInstance().
    c) Eliminate allocations of TypeCollectionContext.

I think that #1423 is no longer needed.

@sungam3r
Copy link
Member

One consideration against static dictionaries. Types can contain metadata. Then a conflict can arise if the user adds different metadata into the built-in types.

@sungam3r
Copy link
Member

Switched to instance fields. This will return some allocations, but still there will be fewer of them than before this PR.

public class GraphTypesLookup
{
private readonly IDictionary<string, IGraphType> _types = new Dictionary<string, IGraphType>();
// Introspection types http://spec.graphql.org/June2018/#sec-Schema-Introspection
private readonly Dictionary<Type, IGraphType> _introspectionTypes = new IGraphType[]
Copy link
Member

Choose a reason for hiding this comment

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

All these types are always added to the schema. Each type is created only once within schema instance. Before these changes types were created several times. It is also true for built-in scalars.

Copy link
Member Author

Choose a reason for hiding this comment

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

👍


// Add introspection types. Note that introspection types rely on the
// CamelCaseNameConverter, as some fields are defined in pascal case - e.g. Field(x => x.Name)
_context = new TypeCollectionContext(
Copy link
Member

Choose a reason for hiding this comment

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

TypeCollectionContext is just like a proxy to resolve/add methods pair. It can be shared across all introspection types whilst AddType calls.

}
if (namedType.IsGenericType)
else if (namedType.IsGenericType && (namedType.ImplementsGenericType(typeof(EdgeType<>)) || namedType.ImplementsGenericType(typeof(ConnectionType<,>))))
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps these types should be taken out of here. I've been thinking about this for a long time. Not in thin PR though.

Copy link
Member Author

Choose a reason for hiding this comment

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

It would be better if upon finding a generic type (e.g. EdgeType<MyModel>), it uses reflection to retrieve the generic version of the type (e.g. EdgeType<>) and then checks _builtInCustomScalars for the generic type. If matched, it will use Activator.CreateInstance to create an instance. Fine as-is though.

Copy link
Member

@sungam3r sungam3r Oct 14, 2020

Choose a reason for hiding this comment

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

Better to make a new issue and link it to #1039

{
type = _builtInScalars.Values.FirstOrDefault(t => t.Name == reference.TypeName);
if (type != null)
this[type.Name] = type;
Copy link
Member

Choose a reason for hiding this comment

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

Example - discovering ID built-in type during schema first for

type Post {
                    id: ID!
                    title: String!
                }

                type Query {
                    post(id: ID!): Post
                }

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 don't use schema-first so I'll trust you here :-)

Copy link
Member

Choose a reason for hiding this comment

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

Wouldn't this also need to check the _builtInCustomScalars? Seems like that would be the case everywhere, it would need to check both wouldn't it?

Copy link
Member

Choose a reason for hiding this comment

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

You are right. Fixed.

@Shane32
Copy link
Member Author

Shane32 commented Oct 14, 2020

Switched to instance fields. This will return some allocations, but still there will be fewer of them than before this PR.

Good. We may want to allow customization of this behavior in a future PR. (Not now.)

@Shane32
Copy link
Member Author

Shane32 commented Oct 14, 2020

This PR looks good to me.

@Shane32
Copy link
Member Author

Shane32 commented Oct 14, 2020

(I can't actually approve the PR since I created it)

@@ -430,7 +430,7 @@ public void can_execute_complex_schema()
}
type Blog {
title: String!
post(id: ID!): Post
post(id: ID!, unused: Long!): Post
Copy link
Member

Choose a reason for hiding this comment

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

One of custom built-in scalars.

Copy link
Member

@sungam3r sungam3r left a comment

Choose a reason for hiding this comment

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

In fact, I approve my own changes 😄 .

@sungam3r
Copy link
Member

This is the last issue in 3.1 milestone. I am very glad that this work has been done. I've wanted to get rid of unused scalars for a very long time, but I've been putting it off all the time. Thanks for the review. Merging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow to override built-in scalars
4 participants