-
Notifications
You must be signed in to change notification settings - Fork 188
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
Split Construction of Graph Types into individual modules #121
base: master
Are you sure you want to change the base?
Split Construction of Graph Types into individual modules #121
Conversation
using System.Collections.Generic; | ||
using System.Linq; | ||
|
||
namespace Example.Repositories |
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.
Please use file-scoped namespaces. We will convert codebase to use filescoped namespaces some time.
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 mean only for new files. Or just leave it as is, not a big deal.
new Dog{ Breed = "Doberman" }, | ||
new Dog{ Breed = "Pit Bull" }, | ||
new Dog{ Breed = "German Shepard" } |
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.
new Dog{ Breed = "Doberman" }, | |
new Dog{ Breed = "Pit Bull" }, | |
new Dog{ Breed = "German Shepard" } | |
new Dog { Breed = "Doberman" }, | |
new Dog { Breed = "Pit Bull" }, | |
new Dog { Breed = "German Shepard" } |
new Cat{ Breed = "Abyssinian" }, | ||
new Cat{ Breed = "American Bobtail" }, | ||
new Cat{ Breed = "Burmese" } |
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.
new Cat{ Breed = "Abyssinian" }, | |
new Cat{ Breed = "American Bobtail" }, | |
new Cat{ Breed = "Burmese" } | |
new Cat { Breed = "Abyssinian" }, | |
new Cat { Breed = "American Bobtail" }, | |
new Cat { Breed = "Burmese" } |
foreach(var dogOperation in dogOperations) | ||
{ | ||
var fields = dogOperation.RegisterFields(); | ||
foreach(var field in fields) |
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.
foreach(var field in fields) | |
foreach (var field in fields) |
{ | ||
public CatQuery(IEnumerable<ICatOperation> catOperations) | ||
{ | ||
foreach(var catOperation in catOperations) |
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.
foreach(var catOperation in catOperations) | |
foreach (var catOperation in catOperations) |
var fields = catOperation.RegisterFields(); | ||
foreach (var field in fields) | ||
{ | ||
AddField((FieldType)field); |
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.
Maybe it's better to use AutoSchema
here.
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.
Can you give me some more details here? Do you have any doc handy for AutoSchema
? I'm thinking it is some way to set things up automatically.
One reason I am doing the way I am right now, is to be non-opinionated on the setup. I give control to the implementor of IQuery
to give me the fields as they see fit. They can subsequently use reflection or any other controlled mechanism to set things up and finally return the fields which I would then attach to the root query. This is especially useful when the application is large, and consumers need several ways to opt in/out of things.
Again, I might be talking about something different but will wait for information on AutoSchema
.
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'm not really sure how the use of AutoSchema
fits into your example. AutoSchema
is useful when creating a type-first schema, where the schema is automatically constructed from CLR types. Your sample does not use those features. Moreover, you are attempting to use methods from multiple classes to assemble a large root graph type. An auto-generated root query type would be contrary to that goal.
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.
You can see a demonstration of a type-first schema here:
https://github.com/graphql-dotnet/graphql-dotnet/tree/master/src/GraphQL.StarWars.TypeFirst
The entire schema can be constructed via:
services.AddGraphQL(b => b
.AddSystemTextJson()
.AddAutoSchema<StarWarsQuery>(c => c.WithMutation<StarWarsMutation>()));
You will notice there are no 'graph types' but rather simple CLR classes decorated with attributes where necessary
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.
In my own graphs, I use a slight variation of this to allow for DI injection of scoped services, which is not possible with the sample seen there. However, the code exists in the tests here:
I also published it as a NuGet package here:
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.
@EmmanuelPonnudurai I'm fine with example as is without AutoSchema
.
|
||
public static class StartupExtensions | ||
{ | ||
public static void AddOperations(this IServiceCollection services) |
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 would inline this method into ConfigureServices.
{ | ||
var fields = new List<IFieldType> | ||
{ | ||
Field<StringGraphType>("say", resolve: context => "woof woof woof"), |
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.
Field<StringGraphType>(
appends field to graph type and returns reference to it.
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.
See comments. I'm done here. Waiting for review from @Shane32 .
I see this is attempting to use DI to build a query (or mutation) type dynamically. This makes sense, although I would not recommend building a set of fields manually (versus using the interface IProvideFields
{
void BuildQueryFields(ObjectGraphType graph) { }
void BuildMutationFields(ObjectGraphType graph) { }
void BuildSubscriptionFields(ObjectGraphType graph) { }
}
class DogHandler : IProvideFields
{
// pull in singleton services here
void IProvideFields.BuildQueryFields(ObjectGraphType graph)
{
graph.Field<Dog>("Dog")
.Argument<int>("id")
.Resolve(ctx => {
// pull scoped services and retrieve dog by id
});
// add "Dogs" or other fields here
}
// optional: implement BuildMutationFields / BuildSubscriptionFields
}
class Query : ObjectGraphType
{
public Query(IEnumerable<IProvdeFields> fieldProviders)
{
foreach (var fp in fieldProviders)
fp.BuildQueryFields(this);
}
}
services.AddSingleton<IProvideFields, DogHandler>();
services.AddSingleton<IProvideFields, CatHandler>(); In such a manner the same handler can be used for query, mutation, and subscription fields. Note that I do not use such a design pattern, but I have no issue providing a demonstration of this or other design patterns. Some day I would like to provide an example of my own design pattern for large schemas. On a side note, the title of the PR seems misleading to me; I typically think of "injecting dependencies" as meaning to assist injecting dependent services into graph types. However, the PR here does not really help as singleton and scoped services still cannot be injected in any other manner than is already available with typical graph construction. Rather, the PR aims to utilize DI to split the construction of the root graph types into individual modules based on the underlying data type it is serving. |
} | ||
catch (Exception ex) | ||
{ | ||
|
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.
@Shane32 Thank you for the feedback. This is completed pls check. One thing I do feel is the lack of an easy way to create a I did search around for it but couldn't find such a method. One wrinkle that I have though with the current approach is that it's not functional in nature. We pass |
|
@EmmanuelPonnudurai First - love your avatar, that is one of my favorite characters. As far as this example goes, and as @Shane32 has kind of already mentioned, I think there are better approaches than what this example is demonstrating. I would highly suggest to avoid coupling dynamic creation of GraphTypes using "mutation" interfaces and "field providers" to "share" field resolution. You can create a small framework to dynamically create GraphTypes, but this example is tightly coupling your domain to the GraphQL.NET framework. That means every time the GraphQL.NET project changes, you could have lots of work to do to get your project upgraded. The Schema-First approach shows how you can almost completely de-couple your field resolvers from the main GraphQL.NET framework. So Schema-First and the Type-First example Shane linked should be preferred over this example. I would highly suggest to avoid using GraphQL.NET types as much as possible in your domain code. Merging this example would seem like this type of approach would be a "suggested" or "sanctioned" usage of this framework, which I think would lead people to having less maintainable code and make it harder for their systems to evolve over time. |
@joemcbride Thanks for the response. And glad to know what you are a Samurai X fan :) When you have 100's of queries and mutations. Individually requiring 100's of domain scoped dependencies. We need to organize things without too much in lining or auto magic. This is my main motive. Some may like the Auto stuff, but I prefer seeing the interaction clearly with the library. Also, if you go with Auto, im sure you will have to end up with some attributes for bailing out of things or fine-tuning stuff as needed. So, it comes down to a preference, I guess. Also, the only touchpoint for me with GraphQL.NET is the I am still using the same things that are required (adding fields), regardless of approach. Only thing is I'm organizing things in such a way that anyone else can add queries and mutations in a clear manner by implementing the necessary interfaces (which is again domain specific but asking you to add Fields) This approach avoids hooking into the abstraction provided by GraphQL.NET by via of Auto Schema or auto things etc. I do agree that this is a bit opinionated. However, I need to see a sample where we have at least 2 different queries and mutations, driven off domain dependencies which can be scoped or transient, and they are doing completely different things. Is there a sample available for this or is it mostly up to the consumer to decide which way they want to do it? I will look at the samples which are shared by @Shane32 Any other pointers for documentation or suggestions are welcome. If I can do it better and still maintain isolation between each operation, that would suffice for me. |
Just my two cents here, but there certainly are different methods to build a large-scale application. One of my medium-size applications has the data models (about 85 of them) and migrations in one library, with no references to GraphQL.NET of any kind. Then there's a service layer, which provides the business logic and other services required by the application. Then there's a GraphQL library, which contains code-first mappings from each data model into graphs, plus mutations with input models, and mappings from those input models back to the data layer. None of the data models are auto-mapped to graphs. Finally, there's the application layer which hosts the ASP.NET Core endpoint. So each of these layers is built on the previous layer. I'm also sure that while my design works fine at its size, I may need another design if I were dealing with a larger application, say of 200-500 data models, as my In another of my applications, again the database models (55 of them) are in its own library, but the models use interfaces and attributes in a well-known manner, such that the GraphQL layer can auto-generate about 95% of the graphs from metadata already known to the database layer. Any graphs requiring special attention are 'overridden' with a custom graph type implementation. This does create a tighter relationship from GraphQL to the database engine, but has huge benefits in terms of simplicity. This is certainly not suitable for everyone, but suited my specific needs with minimal effort, and has almost no maintenance requirements. In some of my smaller applications, I've integrated GraphQL annotations directly on the data types in a type-first approach, but the code can quickly get messy when the data models have intermixed database annotations and graphql annotations, such as @joemcbride was alluding to. However, this might work well if you were building an application small enough that all the code resided in a single project -- perhaps one with only 5-10 data models. I suggest that when your sample application is finished, we be sure to write a few paragraphs explaining the pros and cons of the design approach taken. We might also not wish to overwrite the present sample, but rather to add a new sample to those that are already existing. Note that as this sample is not in the main repository, but rather in the examples repository, I feel it is suitable to provide samples that may not be the generally suggested approach, so long as we have adequate documentation explaining the purpose of the sample. |
I guess I'm not sure what you're asking. You previously mentioned you were looking for a way to construct a |
Agree. I'm fine with either approach while it is documented with pros/cons (not even necessary in detail). |
So is the consensus that I create an entirely different solution for this example. Something like |
The var fieldType = new FieldType();
fieldType.Name = "someName";
fieldType.Type = typeof(NonNullGraphType<StringGraphType>);
fieldType.Resolver = resolverFunc;
return fieldType; My point was that public FieldType CreateField<TGraphType>(string name, string? description = null, QueryArguments? arguments = null, Func<IResolveFieldContext<TSourceType>, object?>? resolve = null, string? deprecationReason = null) where TGraphType : IGraphType; The only difference between existing Again this is a variant which i am looking for to facilitate the way I am structing things. I can create local utility functions to do this kind of work so not the end of the world. |
The See: The suggested pattern for GraphQL.NET v7 is to use the field builders to define fields. Besides matching the current documentation and samples, this allows for extension methods to work properly, such as We also do not suggest creating a // specify by graph type
public static FieldBuilder<object, TReturnType> Field<TGraphType, TReturnType>(this ICollection<FieldType> fields, string name)
where TGraphType : IGraphType
{
var builder = FieldBuilder<object, TReturnType>.Create(typeof(TGraphType)).Name(name);
fields.Add(builder.FieldType);
return builder;
}
// specify by CLR type
public static FieldBuilder<object, TReturnType> Field<TReturnType>(this ICollection<FieldType> fields, string name, bool nullable = false)
{
// note: only output type supported here
var type = typeof(TReturnType).GetGraphTypeFromType(nullable, TypeMappingMode.OutputType);
var builder = FieldBuilder<object, TReturnType>.Create(type).Name(name);
fields.Add(builder.FieldType);
return builder;
} Then your classes could do something like this: public DogQuery : IDogQuery
{
public IEnumerable<FieldType> GetQueryFields()
{
var fields = new List<FieldType>();
fields.Field<Dog>("dog")
.Argument<IdGraphType>("id")
.Resolve()
.WithScope()
.WithService<DogRepository>()
.Resolve((ctx, dogRepository) => dogRepository.GetDog(ctx.GetArgument<int>("id")));
return fields;
}
} I'd probably still pass in a collection, even if it is a new empty one, just so as to eliminate the redundant |
Hello. The examples I have seen thus far are simplistic at best and don't provide a reference for how we need to structure things in a real-world application. This is my attempt to do so.
Especially when we use dependency injection and when there can be a lot of queries and mutations. (Note that I have not added a mutation yet).
We can build on this example and add things so that it's easy for increased adoption or at least serve as a point of reference.
Open to any suggestions and improvements. Thanks in advance.