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.
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.
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.
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 ofObjectType<T>
- Add new fields to the schema type in the form of delegatesC#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.
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.
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();}
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.