How GraphQL Extending Object Types Helps?:
- In GraphQL two major operations are 'Queries' and 'Mutations'.
- So when we think from the code point of view all query-related logics maintain in one file and all mutation-related logics are maintained in another file. That's because GraphQL schema can't accept multiple 'Queries' and 'Mutations'.
- But it is a very tedious job to maintain whole business logic in just 2 files(1Query and 1 Mutation file).
- So to make it simple GraphQL has one technique called 'extend'. The 'extend' GraphQL schema gives us the flexibility to extend the main 'Query' or 'Mutation' which means we can have sub Queries or Mutation those derived from the parent.
- On execution time everything merged as a single Query or Mutation schema.
Hot Chocolate GraphQL Extending Approaches:
Extending Object Types can be done in 2 different approaches:
- Code First
- Pure Code First
Code First:
In the code first approach, every c# class must have a mapping GraphQL class. So on runtime, these GraphQL classes will generate the schema. So in this approach, we need to learn about the GraphQL c# types. We have more control over coding like naming conventions, c# to object mapping, etc.
I had made a couple of blogs on the 'Code First' approach, if interested check the links below:
Pure Code First:
The Pure Code First technique so simple, here we only have c# class. So we have to register C# classes(Query and Mutation classes) in the startup under the GraphQL server. So on run time base on our c# class schema will be generated. This approach is very easy no need to learn any GraphQL terminology, and also we don't have full control over GraphQL code here because everything behind scenes taken care of is the GraphQL server.
Create A .Net5 Web API Project:
Our goal here is to understand Extending Objects Types are helpful to split the large Query and Mutation Classes. So we will implement a simple GraphQL application where we will create 2 Query classes and 2 Mutation classes with the help of Extending Objects Types technique. We also implement one class in 'Code First' and another class in the 'Pure Code First' approach for both Query and Mutation. So let's begin our GraphQL sample by creating .Net5 Web API application.
Note: Hot Chocolate GraphQL supports, coding can be done entirely in the 'Code First' approach or 'Pure Code First' or both combinations.
Install Hot Chocolate Graphql Nuget:
Package Manager Command:
Install-Package HotChocolate.AspNetCore -Version 11.0.9
.Net CLI Command:
dotnet add package HotChocolate.AspNetCore --version 11.0.9
Configure GraphQL Server And Endpoint:
Register GraphQL server in 'ConfigureServices()' method.
Startup.cs:(ConfigureService Method)
services.AddGraphQLServer();Now configure GraphQL endpoint. The default path is '/graphql', if need we can change the path.
Startup.cs:(Configure Method)
app.UseEndpoints(endpoints => { endpoints.MapGraphQL(); endpoints.MapControllers(); });
Register Root Query Name:
Startup.cs:
services.AddGraphQLServer() .AddQueryType(q => q.Name("Query"))Since our goal to have multiple query files, so we have to register GraphQL root query names like 'Query' with 'AddQueryType'. So this name will be used by all Extended Object Types query classes. On runtime, all queries will form a single query schema under the name we specified 'Query'. So we can use any name here, but the 'Query' name looks the most standard.
Create A GraphQL Query In Code First Approach:
Now first query file created in the code first approach. As we have discussed code first approach will have a C# class and its mapping GraphQL class.
Now let's create a C# query class which can be called a Resolver. The 'Resolver' means it contains all query methods like fetching data from the database or any data source. So let's add the folder 'QueryResolvers' and then add c# query file like 'CountryResolver.cs'.
QueryResolvers/CountryResolver.cs:
namespace GQL_Splitfiles.QueryResolvers { public class CountryResolver { public string GetHomeCounry() { return "India"; } } }
- Here I have a method that returns a simple string as a result.
QueryTypes/CountryTypeExtension.cs:
using GQL_Splitfiles.QueryResolvers; using HotChocolate.Types; namespace GQL_Splitfiles.QueryTypes { public class CountryTypeExtension: ObjectTypeExtension { protected override void Configure(IObjectTypeDescriptor descriptor) { descriptor.Name("Query"); descriptor.Field("GetHomeCounry") .ResolveWith<CountryResolver>(_ => _.GetHomeCounry()) .Type<StringType>() .Name("HomeCountry"); } } }
- By inheriting 'HotChocolate.Types.ObjectTypeExtension' makes our 'CountryTypeExtension' as GraphQL extended query.
- (Line: 10) We are registered "Query" as name to 'CoutryTypeExtension'. So in every class that inherits 'ObjectTypeExtension' should register with the name "Query"(the name that registered in the startup file).
- (Line: 12) Every method in the normal C# class(CountryResolver.cs) must be registered as 'Field' in ObjectTypeExtension class(CountryResolver.cs).
- (Line: 13) Using 'ResolveWith' method register the method like 'GetHomeCounry()'.
- (Line: 14) 'SrtingType' is GraphQL type. The 'Type<>' here used to specify the GraphQL type that equivalent to the resolver method registered.
- (Line: 15) The 'Name()' to define the name of our 'Field'. This name will be used by the clients for querying the GraphQL endpoint.
Startup.cs:
services.AddGraphQLServer() .AddQueryType(q => q.Name("Query")) .AddType<CountryTypeExtension>();Now check the GraphQL endpoint and its result.
Create A GraphQL Query In Pure Code First Approach:
Now, 2nd Query file we will implement in the Pure Code First approach. This technique is very simple, a single resolver class is enough.
Now let's create a new resolver file like 'PetResolver.cs'.
QueryResolvers/PetResolver.cs:
using HotChocolate.Types; namespace GQL_Splitfiles.QueryResolvers { [ExtendObjectType(Name="Query")] public class PetResolver { public string YourPet() { return "Dog"; } } }
- The 'HotChocolate.Types.ExtendObjectType' attribute with name "Query", this attribute makes our class 'PetResolver.cs' is extended from the query.
- Now we don't need any mapper of GraphQL class. Because the GraphQL server will automatically generate a query scheme from our resolver class in Pure Code First approach.
Startup.cs:
services.AddGraphQLServer() .AddQueryType(q => q.Name("Query")) .AddType<CountryTypeExtension>() .AddType<PetResolver>();Here in Pure Code First approach, the method name should be in the pascal case while querying. Here in the resolver class method name 'YourPet' need to be written like 'yourPet' while querying.So we have successfully split the Query classes using Extending Object Types.
Add Root Mutation Name:
Startup.cs:
services.AddGraphQLServer() .AddQueryType(q => q.Name("Query")) .AddType<CountryTypeExtension>() .AddType<PetResolver>() .AddMutationType(m => m.Name("Mutation"))Here our goal is to split mutation files, so we have to create a root mutation name like 'Mutation' in the 'AddMutation' method. So this name should be declared in every individual extended mutation file so that all mutation will be combined and generated as a single mutation scheme on runtime.
Create A GraphQL Mutation In Code First Approach:
Now first mutation file created using Code First. So we already discussed in Code First approach every c# class will a mapping GraphQL class.
Mutation means saving or updating data to the database. So we should have a payload object to send data to the server. So let's create a folder like 'Models' and add a file 'CountryModel.cs'.
Models/CountryModel.cs:
namespace GQL_Splitfiles.Models { public class CountryModel { public int Id { get; set; } public string Name { get; set; } } }Now for the 'CountryModel.cs' file, we should have a GraphQL class. So let's create a folder like 'InputTypes' and add a file like 'CountryInput.cs'.
InputTypes/CountryInput.cs:
using GQL_Splitfiles.Models; using HotChocolate.Types; namespace GQL_Splitfiles.InputTypes { public class CountryInput: InputObjectType<CountryModel> { protected override void Configure(IInputObjectTypeDescriptor<CountryModel> descriptor) { descriptor.Name("CountryInput"); descriptor.Field(_ => _.Id) .Type<IntType>() .Name("Id"); descriptor.Field(_ => _.Name) .Type<StringType>() .Name("Name"); } } }
- The 'CountryInput' should Inherit 'InputObjectType<T>'. The 'InputObjectType' is for input data from the client. Inside of the 'CounryInput' we need to register all required properties as 'Fields'. The value in the method 'Name()' will be used by the client while consuming the mutation endpoint.
MutationResolver/CountryMutateResolver.cs:
using GQL_Splitfiles.Models; namespace GQL_Splitfiles.MutationResolvers { public class CountryMutateResolver { public string SaveCountry(CountryModel model) { // implement logic to save to database or datasource return model.Name; } } }Now we should have GraphQL class for 'CoutnryMutateResolver.cs'. So let's create a folder 'MutationTypes' and add a file 'CountryMutateTypeExtension'.
MutationTypes/CountryMutateTypeExtension.cs:
using GQL_Splitfiles.InputTypes; using GQL_Splitfiles.MutationResolvers; using HotChocolate.Types; namespace GQL_Splitfiles.MutationTypes { public class CountryMutateTypeExtension: ObjectTypeExtension { protected override void Configure(IObjectTypeDescriptor descriptor) { descriptor.Name("Mutation"); descriptor.Field("SaveCountry") .ResolveWith<CountryMutateResolver>(m => m.SaveCountry(default)) .Argument("model", _ => _.Type<CountryInput>()) .Type<StringType>() .Name("SaveCountry"); } } }
- (Line: 7) To create an extended mutation, the class must inherit the 'HotChocolate.Types.ObjectTypeExtnesion'.
- (Line: 11) Extended mutation class name as 'Mutation' which should match with the name of the root mutation registered in a startup.
- (Line: 14) Register our resolver method 'SaveCountry()' from 'CountryMutateResolver.cs'.
- (Line: 15) We should register 'SaveCountry()' method input parameters here using 'Argument()' method.
- (Line: 16) GraphQL type whose type equivalent to c# type of 'SaveCountry()' method return type.
- (Line: 17) GraphQL 'name' for our c# 'SaveCountr()' method. This name will be used on the client-side to invoke mutation.
Startup.cs:
services.AddGraphQLServer() .AddQueryType(q => q.Name("Query")) .AddType<CountryTypeExtension>() .AddType<PetResolver>() .AddMutationType(m => m.Name("Mutation")) .AddType<CountryMutateTypeExtension>();Now let's frame the mutation.
Mutation:
mutation ($modelInput:CountryInput){ SaveCountry(model:$modelInput) }
- The 'mutation' keyword just represents the action type.
- The '$modelInput' is GraphQL variable. The 'CountryInput' is type of our $'modelInput'. The 'CountryInput' should match with the value of the 'Name' method inside of the 'CountryInput.cs'.
- The 'SaveCounry' is the registered name of 'Field' in the 'CounryMutateTypeExtension' class. The 'model' is the input variable registered at the 'Argument' method.
{ "modelInput":{ "Id":100, "Name":"India" } }
- Here payload should be passed as properties of an object whose name matches with 'variable in mutation. Here we just remove '$' from the variable name.
Create GraphQL Mutation In Pure Code First Approach:
Now, the 2nd GraphQL Mutation file creating using the Pure Code First Approach. We know that in Pure Code First approach we don't need to create the GraphQL classes.
So first let's create a payload class like 'PoetModel.cs'.
So first let's create a payload class like 'PoetModel.cs'.
Models/PoetModel.cs:
namespace GQL_Splitfiles.Models { public class PetModel { public int Id{get;set;} public string Name{get;set;} } }Now we have to create our Mutation resolver class.
MutaionResolvers/PetMutateResolver.cs:
using GQL_Splitfiles.Models; using HotChocolate.Types; namespace GQL_Splitfiles.MutationResolvers { [ExtendObjectType(Name="Mutation")] public class PetsMutateResolver { public string SavePet(PetModel model) { //logic save data to database return model.Name; } } }
- The 'HotChocolate.Types.ExtendObjectType' make the decorated class extend class of 'Mutation'
Startup.cs:
services.AddGraphQLServer() .AddQueryType(q => q.Name("Query")) .AddType<CountryTypeExtension>() .AddType<PetResolver>() .AddMutationType(m => m.Name("Mutation")) .AddType<CountryMutateTypeExtension>() .AddType<PetsMutateResolver>();Now test the mutation.So we finally split mutation into multiple files.
Video Session:
Support Me!
Buy Me A Coffee
PayPal Me
Wrapping Up:
Hopefully, I think this article delivered some useful information about splitting Query and Mutation files in Hot Chocolate GraphQL. I love to have your feedback, suggestions, and better techniques in the comment section below.
Very Nicely explained.
ReplyDeleteSir, I am using Pure code first approach. I have two question
ReplyDelete1 - I added [ExtendObjectType(Name = "Query")] attribute on my resolver. Everything working fine. But I am getting
Warning CS0618 'ExtendObjectTypeAttribute.Name.set' is obsolete: 'Use the new constructor.' Can you suggest how I can fix this?
2 - I have two resolver GetEmployeeInfoIncludingSalary and
GetEmployeeInfoWithoutSalary both are using same entity Employee.
I created EmployeeType : ObjectType class and in configure method i ignored salary field. so eveything worked for GetEmployeeInfoWithoutSalary.
But when I am calling GetEmployeeInfoIncludingSalary method I am getting error Salary field does not exist. Can we create ObjectType at reslover level so i can inject specific type for specific resolver?
Please Ignore first question, I got solution [ExtendObjectType(name: "Query")]
DeleteYour proposed solution of [ExtendObjectType(name: "Query")] doesn't get rid of the obsolete message for myself. Any other alternatives? The message about it being obsolete doesn't give me enough info about what the new format is.
DeleteTry to replace 'ExtendObjectType' to 'System.Type'
Deletecheck the change log:
https://github.com/ChilliCream/hotchocolate/blob/develop/CHANGELOG.md
How to handle async type of function in resolver?
ReplyDelete