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.

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

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

query: QueryType
me: UserType
name: StringType
company: CompanyType
id: IdType
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 subselections 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 subselections 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.

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

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.

C#
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.

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

The CancellationToken can also be accessed through the IResolverContext.

C#
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
    C#
    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.

C#
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.DependecyInjection. We can access all registered services in our resolvers.

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

C#
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.

C#
public class Query
{
public List<User> GetUsers([Service] UserService userService)
=> 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.

C#
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.

C#
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.

C#
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.

C#
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.

SDL
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.

C#
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.

C#
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.

C#
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.