|
| 1 | +--- |
| 2 | +title: "Resolvers" |
| 3 | +--- |
| 4 | + |
| 5 | +import { ExampleTabs } from "../../../components/mdx/example-tabs" |
| 6 | + |
| 7 | +When it comes to fetching data in a GraphQL server, it will always come down to a resolver. |
| 8 | + |
| 9 | +**A resolver is a generic function that fetches data from an arbitrary data source for a particular field.** |
| 10 | + |
| 11 | +We can think of each field in our query as a method of the previous type which returns the next type. |
| 12 | + |
| 13 | +## Resolver Tree |
| 14 | + |
| 15 | +A resolver tree is a projection of a GraphQL operation that is prepared for execution. |
| 16 | + |
| 17 | +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. |
| 18 | + |
| 19 | +```graphql |
| 20 | +query { |
| 21 | + me { |
| 22 | + name |
| 23 | + company { |
| 24 | + id |
| 25 | + name |
| 26 | + } |
| 27 | + } |
| 28 | +} |
| 29 | +``` |
| 30 | + |
| 31 | +In Hot Chocolate, this query results in the following resolver tree. |
| 32 | + |
| 33 | +```mermaid |
| 34 | +graph LR |
| 35 | + A(query: QueryType) --> B(me: UserType) |
| 36 | + B --> C(name: StringType) |
| 37 | + B --> D(company: CompanyType) |
| 38 | + D --> E(id: IdType) |
| 39 | + D --> F(name: StringType) |
| 40 | +``` |
| 41 | + |
| 42 | +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. |
| 43 | + |
| 44 | +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. |
| 45 | + |
| 46 | +**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.** |
| 47 | + |
| 48 | +The execution of a request finishes, once each resolver of the selected fields has produced a result. |
| 49 | + |
| 50 | +_This is of course an oversimplification that differs from the actual implementation._ |
| 51 | + |
| 52 | +# Defining a Resolver |
| 53 | + |
| 54 | +Resolvers can be defined in a way that should feel very familiar to C# developers, especially in the Annotation-based approach. |
| 55 | + |
| 56 | +## Properties |
| 57 | + |
| 58 | +Hot Chocolate automatically converts properties with a public get accessor to a resolver that simply returns its value. |
| 59 | + |
| 60 | +Properties are also covered in detail by the [object type documentation](/docs/hotchocolate/defining-a-schema/object-types). |
| 61 | + |
| 62 | +## Regular Resolver |
| 63 | + |
| 64 | +A regular resolver is just a simple method, which returns a value. |
| 65 | + |
| 66 | +<ExampleTabs> |
| 67 | +<ExampleTabs.Annotation> |
| 68 | + |
| 69 | +```csharp |
| 70 | +public class Query |
| 71 | +{ |
| 72 | + public string Foo() => "Bar"; |
| 73 | +} |
| 74 | + |
| 75 | +public class Startup |
| 76 | +{ |
| 77 | + public void ConfigureServices(IServiceCollection services) |
| 78 | + { |
| 79 | + services |
| 80 | + .AddGraphQLServer() |
| 81 | + .AddQueryType<Query>(); |
| 82 | + } |
| 83 | +} |
| 84 | +``` |
| 85 | + |
| 86 | +</ExampleTabs.Annotation> |
| 87 | +<ExampleTabs.Code> |
| 88 | + |
| 89 | +```csharp |
| 90 | +public class Query |
| 91 | +{ |
| 92 | + public string Foo() => "Bar"; |
| 93 | +} |
| 94 | + |
| 95 | +public class QueryType: ObjectType<Query> |
| 96 | +{ |
| 97 | + protected override void Configure(IObjectTypeDescriptor<Query> descriptor) |
| 98 | + { |
| 99 | + descriptor |
| 100 | + .Field(f => f.Foo()) |
| 101 | + .Type<NonNullType<StringType>>(); |
| 102 | + } |
| 103 | +} |
| 104 | + |
| 105 | +public class Startup |
| 106 | +{ |
| 107 | + public void ConfigureServices(IServiceCollection services) |
| 108 | + { |
| 109 | + services |
| 110 | + .AddGraphQLServer() |
| 111 | + .AddQueryType<QueryType>(); |
| 112 | + } |
| 113 | +} |
| 114 | +``` |
| 115 | + |
| 116 | +We can also provide a resolver delegate by using the `Resolve` method. |
| 117 | + |
| 118 | +```csharp |
| 119 | +descriptor |
| 120 | + .Field("foo") |
| 121 | + .Resolve(context => |
| 122 | + { |
| 123 | + return "Bar"; |
| 124 | + }); |
| 125 | +``` |
| 126 | + |
| 127 | +</ExampleTabs.Code> |
| 128 | +<ExampleTabs.Schema> |
| 129 | + |
| 130 | +```csharp |
| 131 | +public class Query |
| 132 | +{ |
| 133 | + public string Foo() => "Bar"; |
| 134 | +} |
| 135 | + |
| 136 | +public class Startup |
| 137 | +{ |
| 138 | + public void ConfigureServices(IServiceCollection services) |
| 139 | + { |
| 140 | + services |
| 141 | + .AddGraphQLServer() |
| 142 | + .AddDocumentFromString(@" |
| 143 | + type Query { |
| 144 | + foo: String! |
| 145 | + } |
| 146 | + ") |
| 147 | + .BindComplexType<Query>(); |
| 148 | + } |
| 149 | +} |
| 150 | +``` |
| 151 | + |
| 152 | +We can also add a resolver by calling `AddResolver()` on the `IRequestExecutorBuilder`. |
| 153 | + |
| 154 | +```csharp |
| 155 | +services |
| 156 | + .AddGraphQLServer() |
| 157 | + .AddDocumentFromString(@" |
| 158 | + type Query { |
| 159 | + foo: String! |
| 160 | + } |
| 161 | + ") |
| 162 | + .AddResolver("Query", "foo", (context) => "Bar"); |
| 163 | +``` |
| 164 | + |
| 165 | +</ExampleTabs.Schema> |
| 166 | +</ExampleTabs> |
| 167 | + |
| 168 | +## Async Resolver |
| 169 | + |
| 170 | +Most data fetching operations, like calling a service or communicating with a database, will be asynchronous. |
| 171 | + |
| 172 | +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. |
| 173 | + |
| 174 | +We can also add a `CancellationToken` argument to our resolver. Hot Chocolate will automatically cancel this token if the request has been aborted. |
| 175 | + |
| 176 | +```csharp |
| 177 | +public class Query |
| 178 | +{ |
| 179 | + public async Task<string> Foo(CancellationToken ct) |
| 180 | + { |
| 181 | + // Omitted code for brevity |
| 182 | + } |
| 183 | +} |
| 184 | +``` |
| 185 | + |
| 186 | +When using a delegate resolver, the `CancellationToken` is passed as second argument to the delegate. |
| 187 | + |
| 188 | +```csharp |
| 189 | +descriptor |
| 190 | + .Field("foo") |
| 191 | + .Resolve((context, ct) => |
| 192 | + { |
| 193 | + // Omitted code for brevity |
| 194 | + }); |
| 195 | +``` |
| 196 | + |
| 197 | +The `CancellationToken` can also be accessed through the `IResolverContext`. |
| 198 | + |
| 199 | +```csharp |
| 200 | +descriptor |
| 201 | + .Field("foo") |
| 202 | + .Resolve(context => |
| 203 | + { |
| 204 | + CancellationToken ct = context.RequestAborted; |
| 205 | + |
| 206 | + // Omitted code for brevity |
| 207 | + }); |
| 208 | +``` |
| 209 | + |
| 210 | +## ResolveWith |
| 211 | + |
| 212 | +Thus far we have looked at two ways to specify resolvers in Code-first: |
| 213 | + |
| 214 | +- Add new methods to the CLR type, e.g. the `T` type of `ObjectType<T>` |
| 215 | +- Add new fields to the schema type in the form of delegates |
| 216 | + ```csharp |
| 217 | + descriptor.Field("foo").Resolve(context => ) |
| 218 | + ``` |
| 219 | + |
| 220 | +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. |
| 221 | + |
| 222 | +```csharp |
| 223 | +public class FooResolvers |
| 224 | +{ |
| 225 | + public string GetFoo(string arg, [Service] FooService service) |
| 226 | + { |
| 227 | + // Omitted code for brevity |
| 228 | + } |
| 229 | +} |
| 230 | + |
| 231 | +public class QueryType : ObjectType |
| 232 | +{ |
| 233 | + protected override void Configure(IObjectTypeDescriptor descriptor) |
| 234 | + { |
| 235 | + descriptor |
| 236 | + .Field("foo") |
| 237 | + .Argument("arg", a => a.Type<NonNullType<StringType>>()) |
| 238 | + .ResolveWith<FooResolvers>(r => r.GetFoo(default, default)); |
| 239 | + } |
| 240 | +} |
| 241 | +``` |
| 242 | + |
| 243 | +# Arguments |
| 244 | + |
| 245 | +We can access arguments we defined for our resolver like regular arguments of a function. |
| 246 | + |
| 247 | +There are also specific arguments that will be automatically populated by Hot Chocolate when the resolver is executed. These include [Dependency injection services](#injecting-services), [DataLoaders](/docs/hotchocolate/fetching-data/dataloader), state, or even context like a [_parent_](#accessing-parent-values) value. |
| 248 | + |
| 249 | +[Learn more about arguments](/docs/hotchocolate/defining-a-schema/arguments) |
| 250 | + |
| 251 | +# Injecting Services |
| 252 | + |
| 253 | +Resolvers integrate nicely with `Microsoft.Extensions.DependecyInjection`. |
| 254 | +We can access all registered services in our resolvers. |
| 255 | + |
| 256 | +Let's assume we have created a `UserService` and registered it as a service. |
| 257 | + |
| 258 | +```csharp |
| 259 | +public class Startup |
| 260 | +{ |
| 261 | + public void ConfigureServices(IServiceCollection services) |
| 262 | + { |
| 263 | + services.AddSingleton<UserService>() |
| 264 | + |
| 265 | + services |
| 266 | + .AddGraphQLServer() |
| 267 | + .AddQueryType<Query>(); |
| 268 | + } |
| 269 | +} |
| 270 | +``` |
| 271 | + |
| 272 | +We can then access the `UserService` in our resolvers like the following. |
| 273 | + |
| 274 | +<ExampleTabs> |
| 275 | +<ExampleTabs.Annotation> |
| 276 | + |
| 277 | +```csharp |
| 278 | +public class Query |
| 279 | +{ |
| 280 | + public List<User> GetUsers([Service] UserService userService) |
| 281 | + => userService.GetUsers(); |
| 282 | +} |
| 283 | +``` |
| 284 | + |
| 285 | +</ExampleTabs.Annotation> |
| 286 | +<ExampleTabs.Code> |
| 287 | + |
| 288 | +```csharp |
| 289 | +public class Query |
| 290 | +{ |
| 291 | + public List<User> GetUsers([Service] UserService userService) |
| 292 | + => userService.GetUsers(); |
| 293 | +} |
| 294 | + |
| 295 | +public class QueryType: ObjectType<Query> |
| 296 | +{ |
| 297 | + protected override void Configure(IObjectTypeDescriptor<Query> descriptor) |
| 298 | + { |
| 299 | + descriptor |
| 300 | + .Field(f => f.Foo(default)) |
| 301 | + .Type<ListType<UserType>>(); |
| 302 | + } |
| 303 | +} |
| 304 | +``` |
| 305 | + |
| 306 | +When using the `Resolve` method, we can access services through the `IResolverContext`. |
| 307 | + |
| 308 | +```csharp |
| 309 | +descriptor |
| 310 | + .Field("foo") |
| 311 | + .Resolve(context => |
| 312 | + { |
| 313 | + var userService = context.Service<UserService>(); |
| 314 | + |
| 315 | + return userService.GetUsers(); |
| 316 | + }); |
| 317 | +``` |
| 318 | + |
| 319 | +</ExampleTabs.Code> |
| 320 | +<ExampleTabs.Schema> |
| 321 | + |
| 322 | +```csharp |
| 323 | +public class Query |
| 324 | +{ |
| 325 | + public List<User> GetUsers([Service] UserService userService) |
| 326 | + => userService.GetUsers(); |
| 327 | +} |
| 328 | +``` |
| 329 | + |
| 330 | +When using `AddResolver()`, we can access services through the `IResolverContext`. |
| 331 | + |
| 332 | +```csharp |
| 333 | +services |
| 334 | + .AddGraphQLServer() |
| 335 | + .AddDocumentFromString(@" |
| 336 | + type Query { |
| 337 | + users: [User!]! |
| 338 | + } |
| 339 | + ") |
| 340 | + .AddResolver("Query", "users", (context) => |
| 341 | + { |
| 342 | + var userService = context.Service<UserService>(); |
| 343 | + |
| 344 | + return userService.GetUsers(); |
| 345 | + }); |
| 346 | +``` |
| 347 | + |
| 348 | +</ExampleTabs.Schema> |
| 349 | +</ExampleTabs> |
| 350 | + |
| 351 | +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. |
| 352 | + |
| 353 | +## Constructor Injection |
| 354 | + |
| 355 | +Of course we can also inject services into the constructor of our types. |
| 356 | + |
| 357 | +```csharp |
| 358 | +public class Query |
| 359 | +{ |
| 360 | + private readonly UserService _userService; |
| 361 | + |
| 362 | + public Query(UserService userService) |
| 363 | + { |
| 364 | + _userService = userService; |
| 365 | + } |
| 366 | + |
| 367 | + public List<User> GetUsers() |
| 368 | + => _userService.GetUsers(); |
| 369 | +} |
| 370 | +``` |
| 371 | + |
| 372 | +It's important to note that the service lifetime of types is singleton per default for performance reasons. |
| 373 | + |
| 374 | +**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.** |
| 375 | + |
| 376 | +If we depend on truly transient or scoped services, we need to inject them directly into the dependent methods as described [above](#injecting-services). |
| 377 | + |
| 378 | +[Learn more about service lifetimes in ASP.NET Core](https://docs.microsoft.com/dotnet/core/extensions/dependency-injection#service-lifetimes) |
| 379 | + |
| 380 | +## IHttpContextAccessor |
| 381 | + |
| 382 | +The [IHttpContextAccessor](https://docs.microsoft.com/dotnet/api/microsoft.aspnetcore.http.ihttpcontextaccessor) allows us to access the [HttpContext](https://docs.microsoft.com/dotnet/api/microsoft.aspnetcore.http.httpcontext) of the current request from within our resolvers. This is useful, if we for example need to set a header or cookie. |
| 383 | + |
| 384 | +First we need to add the `IHttpContextAccessor` as a service. |
| 385 | + |
| 386 | +```csharp |
| 387 | +public class Startup |
| 388 | +{ |
| 389 | + public void ConfigureServices(IServiceCollection services) |
| 390 | + { |
| 391 | + services.AddHttpContextAccessor(); |
| 392 | + |
| 393 | + // Omitted code for brevity |
| 394 | + } |
| 395 | +} |
| 396 | +``` |
| 397 | + |
| 398 | +After this we can inject it into our resolvers and make use of the the `HttpContext` property. |
| 399 | + |
| 400 | +```csharp |
| 401 | +public string Foo(string id, [Service] IHttpContextAccessor httpContextAccessor) |
| 402 | +{ |
| 403 | + if (httpContextAccessor.HttpContext is not null) |
| 404 | + { |
| 405 | + // Omitted code for brevity |
| 406 | + } |
| 407 | +} |
| 408 | +``` |
| 409 | + |
| 410 | +## IResolverContext |
| 411 | + |
| 412 | +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. |
| 413 | + |
| 414 | +```csharp |
| 415 | +public class Query |
| 416 | +{ |
| 417 | + public string Foo(IResolverContext context) |
| 418 | + { |
| 419 | + // Omitted code for brevity |
| 420 | + } |
| 421 | +} |
| 422 | +``` |
| 423 | + |
| 424 | +# Accessing parent values |
| 425 | + |
| 426 | +The resolver of each field on a type has access to the value that was resolved for said type. |
| 427 | + |
| 428 | +Let's look at an example. We have the following schema. |
| 429 | + |
| 430 | +```sdl |
| 431 | +type Query { |
| 432 | + me: User!; |
| 433 | +} |
| 434 | +
|
| 435 | +type User { |
| 436 | + id: ID!; |
| 437 | + friends: [User!]!; |
| 438 | +} |
| 439 | +``` |
| 440 | + |
| 441 | +The `User` schema type is represented by an `User` CLR type. The `id` field is an actual property on this CLR type. |
| 442 | + |
| 443 | +```csharp |
| 444 | +public class User |
| 445 | +{ |
| 446 | + public string Id { get; set; } |
| 447 | +} |
| 448 | +``` |
| 449 | + |
| 450 | +`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. |
| 451 | +From the point of view of this `friends` resolver, the `User` CLR type is its _parent_. |
| 452 | + |
| 453 | +We can access this so called _parent_ value like the following. |
| 454 | + |
| 455 | +<ExampleTabs> |
| 456 | +<ExampleTabs.Annotation> |
| 457 | + |
| 458 | +In the Annotation-based approach we can just access the properties using the `this` keyword. |
| 459 | + |
| 460 | +```csharp |
| 461 | +public class User |
| 462 | +{ |
| 463 | + public string Id { get; set; } |
| 464 | + |
| 465 | + public List<User> GetFriends() |
| 466 | + { |
| 467 | + var currentUserId = this.Id; |
| 468 | + |
| 469 | + // Omitted code for brevity |
| 470 | + } |
| 471 | +} |
| 472 | +``` |
| 473 | + |
| 474 | +There's also a `[Parent]` attribute that injects the parent into the resolver. |
| 475 | + |
| 476 | +```csharp |
| 477 | +public class User |
| 478 | +{ |
| 479 | + public string Id { get; set; } |
| 480 | + |
| 481 | + public List<User> GetFriends([Parent] User parent) |
| 482 | + { |
| 483 | + // Omitted code for brevity |
| 484 | + } |
| 485 | +} |
| 486 | +``` |
| 487 | + |
| 488 | +This is especially useful when using [type extensions](/docs/hotchocolate/defining-a-schema/extending-types). |
| 489 | + |
| 490 | +</ExampleTabs.Annotation> |
| 491 | +<ExampleTabs.Code> |
| 492 | + |
| 493 | +```csharp |
| 494 | +public class User |
| 495 | +{ |
| 496 | + public string Id { get; set; } |
| 497 | + |
| 498 | + public List<User> GetFriends([Parent] User parent) |
| 499 | + { |
| 500 | + // Omitted code for brevity |
| 501 | + } |
| 502 | +} |
| 503 | +``` |
| 504 | + |
| 505 | +When using the `Resolve` method, we can access the parent through the `IResolverContext`. |
| 506 | + |
| 507 | +```csharp |
| 508 | +public class User |
| 509 | +{ |
| 510 | + public string Id { get; set; } |
| 511 | +} |
| 512 | + |
| 513 | +public class UserType : ObjectType<User> |
| 514 | +{ |
| 515 | + protected override void Configure(IObjectTypeDescriptor<User> descriptor) |
| 516 | + { |
| 517 | + descriptor |
| 518 | + .Field("friends") |
| 519 | + .Resolve(context => |
| 520 | + { |
| 521 | + User parent = context.Parent<User>(); |
| 522 | + |
| 523 | + // Omitted code for brevity |
| 524 | + }); |
| 525 | + } |
| 526 | +} |
| 527 | +``` |
| 528 | + |
| 529 | +</ExampleTabs.Code> |
| 530 | +<ExampleTabs.Schema> |
| 531 | + |
| 532 | +```csharp |
| 533 | +public class User |
| 534 | +{ |
| 535 | + public string Id { get; set; } |
| 536 | + |
| 537 | + public List<User> GetFriends([Parent] User parent) |
| 538 | + { |
| 539 | + // Omitted code for brevity |
| 540 | + } |
| 541 | +} |
| 542 | +``` |
| 543 | + |
| 544 | +When using `AddResolver()`, we can access the parent through the `IResolverContext`. |
| 545 | + |
| 546 | +```csharp |
| 547 | +services |
| 548 | + .AddGraphQLServer() |
| 549 | + .AddDocumentFromString(@" |
| 550 | + type User { |
| 551 | + friends: [User!]! |
| 552 | + } |
| 553 | + ") |
| 554 | + .AddResolver("User", "friends", (context) => |
| 555 | + { |
| 556 | + User parent = context.Parent<User>(); |
| 557 | + |
| 558 | + // Omitted code for brevity |
| 559 | + }); |
| 560 | +``` |
| 561 | + |
| 562 | +</ExampleTabs.Schema> |
| 563 | +</ExampleTabs> |
0 commit comments