Skip to content

Latest commit

 

History

History
527 lines (405 loc) · 12.2 KB

File metadata and controls

527 lines (405 loc) · 12.2 KB
title
Object Types

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; }
}

In the Code-first approach we create a new class inheriting from ObjectType<T> to map our POCO Author to an object type.

public class Author
{
    public string Name { get; set; }
}

public class AuthorType : ObjectType<Author>
{
}

We can override the Configure method to have access to an IObjectTypeDescriptor through which we can configure the object type.

public class AuthorType : ObjectType<Author>
{
    protected override void Configure(IObjectTypeDescriptor<Author> descriptor)
    {

    }
}

The descriptor gives us the ability to configure the object type. We will cover how to use it in the following chapters.

Since there could be multiple types inheriting from ObjectType<Author>, but differing in their name and fields, it is not certain which of these types should be used when we return an Author CLR type from one of our resolvers.

Therefore it's important to note that Code-first object types are not automatically inferred. They need to be explicitly specified or registered.

We can either explicitly specify the type on a per-resolver basis or we can register the type once globally:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services
            .AddGraphQLServer()
            .AddType<AuthorType>();
    }
}

In the above example every Author CLR type we return from our resolvers would be assumed to be an AuthorType.

We can also create schema object types without a backing POCO.

public class AuthorType : ObjectType
{
    protected override void Configure(IObjectTypeDescriptor descriptor)
    {

    }
}

Head over here to learn how to add fields to such a type.

public class Author
{
    public string Name { get; set; }
}

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services
            .AddGraphQLServer()
            .AddDocumentFromString(@"
                type Author {
                  name: String
                }
            ")
            .BindComplexType<Author>();
    }
}

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; }
}

In the Code-first approach we can ignore certain properties of our POCO using the Ignore method on the descriptor. This is only necessary, if the binding behavior of the object type is implicit.

public class BookType : ObjectType<Book>
{
    protected override void Configure(IObjectTypeDescriptor<Book> descriptor)
    {
        descriptor.Ignore(f => f.Title);
    }
}

We do not have to ignore fields in the Schema-first approach.

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.BindFieldsExplicitly();

        descriptor.Field(f => f.Title);
    }
}

Naming

Unless specified explicitly, Hot Chocolate automatically infers the names of object types and their fields. Per default the name of the class becomes the name of the object type. When using ObjectType<T> in Code-first, the name of T is chosen as the name for the object type. The names of methods and properties on the respective class are chosen as names of the fields of the object type.

The following conventions are applied when transforming C# method and property names into GraphQL fields:

  • Get prefixes are removed: The get operation is implied and therefore redundant information.
  • Async postfixes are removed: The Async is an implementation detail and therefore not relevant to the schema.

With the name cleaned of unnecessary information, we also change the casing at the start of the name:

FooBar --> fooBar
IPAddress --> ipAddress
PLZ --> plz

Of course, we can also explicitly specify the names in the resulting GraphQL schema:

The [GraphQLName] attribute allows us to specify an explicit name.

[GraphQLName("BookAuthor")]
public class Author
{
    [GraphQLName("fullName")]
    public string Name { get; set; }
}

The Name method on the IObjectTypeDescriptor / IObjectFieldDescriptor allows us to specify an explicit name.

public class AuthorType : ObjectType<Author>
{
    protected override void Configure(IObjectTypeDescriptor<Author> descriptor)
    {
        descriptor.Name("BookAuthor");

        descriptor
            .Field(f => f.Name)
            .Name("fullName");
    }
}

Simply change the names in the schema.

This would produce the following BookAuthor schema object type:

type BookAuthor {
  fullName: String
}

If only one of our clients requires specific names, it is better to use aliases in this client's operations than changing the entire schema.

{
  MyUser: user {
    Username: name
  }
}

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 or Code-first types in general.

In the annotation-based approach we can use the [GraphQLType] attribute.

public class Author
{
    [GraphQLType(typeof(StringType))]
    public string Name { get; set; }
}

In the Code-first approach we can use the Type<T> method on the IObjectFieldDescriptor.

public class AuthorType : ObjectType<Author>
{
    protected override void Configure(IObjectTypeDescriptor<Author> descriptor)
    {
        descriptor
            .Field(f => f.Name)
            .Type<StringType>();
    }
}

Simply change the field type in the schema.

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
    }
}

In the Code-first approach we can use the Resolve method on the IObjectFieldDescriptor.

public class AuthorType : ObjectType<Author>
{
    protected override void Configure(IObjectTypeDescriptor<Author> descriptor)
    {
        descriptor
            .Field("AdditionalField")
            .Resolve(context =>
            {
                // Omitted code for brevity
            })
    }
}
public class Author
{
    public string Name { get; set; }
}

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services
            .AddGraphQLServer()
            .AddDocumentFromString(@"
                type Author {
                  name: String
                  additionalField: DateTime!
                }
            ")
            .BindComplexType<Author>()
            .AddResolver("Author", "additionalField", (context) =>
            {
                // 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.

Learn more about resolvers

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>();
    }
}