Skip to content

Latest commit

 

History

History
562 lines (433 loc) · 13.3 KB

File metadata and controls

562 lines (433 loc) · 13.3 KB
title
Resolvers

When it comes to fetching data in a GraphQL server, it will always come down to a resolver.

A resolver is a generic function that fetches data from an arbitrary data source for a particular field.

We can think of each field in our query as a method of the previous type which returns the next type.

Resolver Tree

A resolver tree is a projection of a GraphQL operation that is prepared for execution.

For better understanding, let's imagine we have a simple GraphQL query like the following, where we select some fields of the currently logged-in user.

query {
  me {
    name
    company {
      id
      name
    }
  }
}

In Hot Chocolate, this query results in the following resolver tree.

graph LR
  A(query: QueryType) --> B(me: UserType)
  B --> C(name: StringType)
  B --> D(company: CompanyType)
  D --> E(id: IdType)
  D --> F(name: StringType)

This tree will be traversed by the execution engine, starting with one or more root resolvers. In the above example the me field represents the only root resolver.

Field resolvers that are sub-selections of a field, can only be executed after a value has been resolved for their parent field. In the case of the above example this means that the name and company resolvers can only run, after the me resolver has finished. Resolvers of field sub-selections can and will be executed in parallel.

Because of this it is important that resolvers, with the exception of top level mutation field resolvers, do not contain side-effects, since their execution order may vary.

The execution of a request finishes, once each resolver of the selected fields has produced a result.

This is of course an oversimplification that differs from the actual implementation.

Defining a Resolver

Resolvers can be defined in a way that should feel very familiar to C# developers, especially in the Annotation-based approach.

Properties

Hot Chocolate automatically converts properties with a public get accessor to a resolver that simply returns its value.

Properties are also covered in detail by the object type documentation.

Regular Resolver

A regular resolver is just a simple method, which returns a value.

public class Query
{
    public string Foo() => "Bar";
}

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services
            .AddGraphQLServer()
            .AddQueryType<Query>();
    }
}
public class Query
{
    public string Foo() => "Bar";
}

public class QueryType: ObjectType<Query>
{
    protected override void Configure(IObjectTypeDescriptor<Query> descriptor)
    {
        descriptor
            .Field(f => f.Foo())
            .Type<NonNullType<StringType>>();
    }
}

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

We can also provide a resolver delegate by using the Resolve method.

descriptor
    .Field("foo")
    .Resolve(context =>
    {
        return "Bar";
    });
public class Query
{
    public string Foo() => "Bar";
}

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

We can also add a resolver by calling AddResolver() on the IRequestExecutorBuilder.

services
    .AddGraphQLServer()
    .AddDocumentFromString(@"
        type Query {
          foo: String!
        }
    ")
    .AddResolver("Query", "foo", (context) => "Bar");

Async Resolver

Most data fetching operations, like calling a service or communicating with a database, will be asynchronous.

In Hot Chocolate, we can simply mark our resolver methods and delegates as async or return a Task<T> and it becomes an async-capable resolver.

We can also add a CancellationToken argument to our resolver. Hot Chocolate will automatically cancel this token if the request has been aborted.

public class Query
{
    public async Task<string> Foo(CancellationToken ct)
    {
        // Omitted code for brevity
    }
}

When using a delegate resolver, the CancellationToken is passed as second argument to the delegate.

descriptor
    .Field("foo")
    .Resolve((context, ct) =>
    {
        // Omitted code for brevity
    });

The CancellationToken can also be accessed through the IResolverContext.

descriptor
    .Field("foo")
    .Resolve(context =>
    {
        CancellationToken ct = context.RequestAborted;

        // Omitted code for brevity
    });

ResolveWith

Thus far we have looked at two ways to specify resolvers in Code-first:

  • Add new methods to the CLR type, e.g. the T type of ObjectType<T>

  • Add new fields to the schema type in the form of delegates

    descriptor.Field("foo").Resolve(context => )

But there's a third way. We can describe our field using the descriptor, but instead of a resolver delegate, we can point to a method on another class, responsible for resolving this field.

public class FooResolvers
{
    public string GetFoo(string arg, [Service] FooService service)
    {
        // Omitted code for brevity
    }
}

public class QueryType : ObjectType
{
    protected override void Configure(IObjectTypeDescriptor descriptor)
    {
        descriptor
            .Field("foo")
            .Argument("arg", a => a.Type<NonNullType<StringType>>())
            .ResolveWith<FooResolvers>(r => r.GetFoo(default, default));
    }
}

Arguments

We can access arguments we defined for our resolver like regular arguments of a function.

There are also specific arguments that will be automatically populated by Hot Chocolate when the resolver is executed. These include Dependency injection services, DataLoaders, state, or even context like a parent value.

Learn more about arguments

Injecting Services

Resolvers integrate nicely with Microsoft.Extensions.DependencyInjection. We can access all registered services in our resolvers.

Let's assume we have created a UserService and registered it as a service.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<UserService>()

        services
            .AddGraphQLServer()
            .AddQueryType<Query>();
    }
}

We can then access the UserService in our resolvers like the following.

public class Query
{
    public List<User> GetUsers([Service] UserService userService)
        => userService.GetUsers();
}
public class Query
{
    public List<User> GetUsers([Service] UserService userService)
        => userService.GetUsers();
}

public class QueryType: ObjectType<Query>
{
    protected override void Configure(IObjectTypeDescriptor<Query> descriptor)
    {
        descriptor
            .Field(f => f.Foo(default))
            .Type<ListType<UserType>>();
    }
}

When using the Resolve method, we can access services through the IResolverContext.

descriptor
    .Field("foo")
    .Resolve(context =>
    {
        var userService = context.Service<UserService>();

        return userService.GetUsers();
    });
public class Query
{
    public List<User> GetUsers([Service] UserService userService)
        => userService.GetUsers();
}

When using AddResolver(), we can access services through the IResolverContext.

services
    .AddGraphQLServer()
    .AddDocumentFromString(@"
        type Query {
          users: [User!]!
        }
    ")
    .AddResolver("Query", "users", (context) =>
    {
        var userService = context.Service<UserService>();

        return userService.GetUsers();
    });

Hot Chocolate will correctly inject the service depending on its lifetime. For example, a scoped service is only instantiated once per scope (by default that's the GraphQL request execution) and this same instance is injected into all resolvers who share the same scope.

Constructor Injection

Of course we can also inject services into the constructor of our types.

public class Query
{
    private readonly UserService _userService;

    public Query(UserService userService)
    {
        _userService = userService;
    }

     public List<User> GetUsers()
        => _userService.GetUsers();
}

It's important to note that the service lifetime of types is singleton per default for performance reasons.

This means one instance per injected service is kept around and used for the entire lifetime of the GraphQL server, regardless of the original lifetime of the service.

If we depend on truly transient or scoped services, we need to inject them directly into the dependent methods as described above.

Learn more about service lifetimes in ASP.NET Core

IHttpContextAccessor

The IHttpContextAccessor allows us to access the HttpContext of the current request from within our resolvers. This is useful, if we for example need to set a header or cookie.

First we need to add the IHttpContextAccessor as a service.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpContextAccessor();

        // Omitted code for brevity
    }
}

After this we can inject it into our resolvers and make use of the the HttpContext property.

public string Foo(string id, [Service] IHttpContextAccessor httpContextAccessor)
{
    if (httpContextAccessor.HttpContext is not null)
    {
        // Omitted code for brevity
    }
}

IResolverContext

The IResolverContext is mainly used in delegate resolvers of the Code-first approach, but we can also access it in the Annotation-based approach, by simply injecting it.

public class Query
{
    public string Foo(IResolverContext context)
    {
        // Omitted code for brevity
    }
}

Accessing parent values

The resolver of each field on a type has access to the value that was resolved for said type.

Let's look at an example. We have the following schema.

type Query {
  me: User!;
}

type User {
  id: ID!;
  friends: [User!]!;
}

The User schema type is represented by an User CLR type. The id field is an actual property on this CLR type.

public class User
{
    public string Id { get; set; }
}

friends on the other hand is a resolver i.e. method we defined. It depends on the user's Id property to compute its result. From the point of view of this friends resolver, the User CLR type is its parent.

We can access this so called parent value like the following.

In the Annotation-based approach we can just access the properties using the this keyword.

public class User
{
    public string Id { get; set; }

    public List<User> GetFriends()
    {
        var currentUserId = this.Id;

        // Omitted code for brevity
    }
}

There's also a [Parent] attribute that injects the parent into the resolver.

public class User
{
    public string Id { get; set; }

    public List<User> GetFriends([Parent] User parent)
    {
        // Omitted code for brevity
    }
}

This is especially useful when using type extensions.

public class User
{
    public string Id { get; set; }

    public List<User> GetFriends([Parent] User parent)
    {
        // Omitted code for brevity
    }
}

When using the Resolve method, we can access the parent through the IResolverContext.

public class User
{
    public string Id { get; set; }
}

public class UserType : ObjectType<User>
{
    protected override void Configure(IObjectTypeDescriptor<User> descriptor)
    {
        descriptor
            .Field("friends")
            .Resolve(context =>
            {
                User parent = context.Parent<User>();

                // Omitted code for brevity
            });
    }
}
public class User
{
    public string Id { get; set; }

    public List<User> GetFriends([Parent] User parent)
    {
        // Omitted code for brevity
    }
}

When using AddResolver(), we can access the parent through the IResolverContext.

services
    .AddGraphQLServer()
    .AddDocumentFromString(@"
        type User {
          friends: [User!]!
        }
    ")
    .AddResolver("User", "friends", (context) =>
    {
        User parent = context.Parent<User>();

        // Omitted code for brevity
    });