The most important type in a GraphQL schema is the object type. It contains fields that can return simple scalars like String
, Int
, or again object types.
type Author { name: String}
type Book { title: String author: Author}
Learn more about object types here.
Usage
Object types can be defined like the following.
In the Annotation-based approach we are essentially just creating regular C# classes.
public class Author{ public string Name { get; set; }}
Binding behavior
In the Annotation-based approach all public properties and methods are implicitly mapped to fields of the schema object type.
In the Code-first approach we have a little more control over this behavior. By default all public properties and methods of our POCO are mapped to fields of the schema object type. This behavior is called implicit binding. There is also an explicit binding behavior, where we have to opt-in properties we want to include.
We can configure our preferred binding behavior globally like the following.
services .AddGraphQLServer() .ModifyOptions(options => { options.DefaultBindingBehavior = BindingBehavior.Explicit; });
We can also override it on a per type basis:
public class BookType : ObjectType<Book>{ protected override void Configure(IObjectTypeDescriptor<Book> descriptor) { descriptor.BindFields(BindingBehavior.Implicit);
// We could also use the following methods respectively // descriptor.BindFieldsExplicitly(); // descriptor.BindFieldsImplicitly(); }}
Ignoring fields
In the Annotation-based approach we can ignore fields using the [GraphQLIgnore]
attribute.
public class Book{ [GraphQLIgnore] public string Title { get; set; }
public Author Author { get; set; }}
Including fields
In the Code-first approach we can explicitly include certain properties of our POCO using the Field
method on the descriptor
. This is only necessary, if the binding behavior of the object type is explicit.
public class BookType : ObjectType<Book>{ protected override void Configure(IObjectTypeDescriptor<Book> descriptor) { descriptor.Field(f => f.Title); }}
Naming
Hot Chocolate infers the names of the object types and their fields automatically, but sometimes we might want to specify names ourselves.
Per default the name of the class is the name of the object type in the schema and the names of the properties are the names of the fields of that object type.
We can override these defaults using the [GraphQLName]
attribute.
[GraphQLName("BookAuthor")]public class Author{ [GraphQLName("fullName")] public string Name { get; set; }}
This would produce the following BookAuthor
schema object type:
type BookAuthor { fullName: String}
Explicit types
Hot Chocolate will, most of the time, correctly infer the schema types of our fields. Sometimes we might have to be explicit about it though, for example when we are working with custom scalars.
In the annotation-based approach we can use the [GraphQLType]
attribute.
public class Author{ [GraphQLType(typeof(StringType))] public string Name { get; set; }}
Additional fields
We can add additional (dynamic) fields to our schema types, without adding new properties to our backing class.
public class Author{ public string Name { get; set; }
public DateTime AdditionalField() { // Omitted code for brevity }}
What we have just created is a resolver. Hot Chocolate automatically creates resolvers for our properties, but we can also define them ourselves.
Head over to the resolver documentation to learn more.
Generics
Note: Read about interfaces and unions before resorting to generic object types.
In the Code-first approach we can define generic object types.
public class Response{ public string Status { get; set; }
public object Payload { get; set; }}
public class ResponseType<T> : ObjectType<Response> where T : class, IOutputType{ protected override void Configure( IObjectTypeDescriptor<Response> descriptor) { descriptor.Field(f => f.Status);
descriptor .Field(f => f.Payload) .Type<T>(); }}
public class Query{ public Response GetResponse() { return new Response { Status = "OK", Payload = 123 }; }}
public class QueryType : ObjectType<Query>{ protected override void Configure(IObjectTypeDescriptor<Query> descriptor) { descriptor .Field(f => f.GetResponse()) .Type<ResponseType<IntType>>(); }}
This will produce the following schema types.
type Query { response: Response}
type Response { status: String! payload: Int}
We have used an object
as the generic field above, but we can also make Response
generic and add another generic parameter to the ResponseType
.
public class Response<T>{ public string Status { get; set; }
public T Payload { get; set; }}
public class ResponseType<TSchemaType, TRuntimeType> : ObjectType<Response<TRuntimeType>> where TSchemaType : class, IOutputType{ protected override void Configure( IObjectTypeDescriptor<Response<TRuntimeType>> descriptor) { descriptor.Field(f => f.Status);
descriptor .Field(f => f.Payload) .Type<TSchemaType>(); }}
Naming
If we were to use the above type with two different generic arguments, we would get an error, since both ResponseType
have the same name.
We can change the name of our generic object type depending on the used generic type.
public class ResponseType<T> : ObjectType<Response> where T : class, IOutputType{ protected override void Configure( IObjectTypeDescriptor<Response> descriptor) { descriptor .Name(dependency => dependency.Name + "Response") .DependsOn<T>();
descriptor.Field(f => f.Status);
descriptor .Field(f => f.Payload) .Type<T>(); }}