|
1 | 1 | using System; |
2 | 2 | using System.Collections.Generic; |
3 | 3 | using System.Linq; |
| 4 | +using System.Linq.Expressions; |
4 | 5 | using EntityGraphQL.Schema; |
5 | 6 | using Microsoft.Extensions.DependencyInjection; |
6 | 7 | using Xunit; |
@@ -516,4 +517,96 @@ public void ServiceResolvedPage_ItemsWithComplexResolveBulkKey_Works() |
516 | 517 | Assert.Equal("Alice", page.items[0].createdByName); |
517 | 518 | Assert.Equal("Bob", page.items[1].createdByName); |
518 | 519 | } |
| 520 | + /// <summary> |
| 521 | + /// The two-pass flow exists so a bulk resolver can collect its keys from the materialized rows. A |
| 522 | + /// selection with nothing to bulk load stays on the single-pass path - the extra pass would be an extra |
| 523 | + /// compile and a projection of every row for no gain. |
| 524 | + /// </summary> |
| 525 | + [Fact] |
| 526 | + public void ServiceRootList_WithoutBulkFieldSelected_RunsInOnePass() |
| 527 | + { |
| 528 | + var (schema, sp, _, names, _) = BuildApiKeysSchema(apiKey => |
| 529 | + { |
| 530 | + apiKey |
| 531 | + .AddField("createdByName", "Creator display name") |
| 532 | + .Resolve<CreatedByNameService>((k, svc) => svc.GetName(k.CreatedById)) |
| 533 | + .ResolveBulk<CreatedByNameService, string, string?>(k => k.CreatedById!, (ids, svc) => svc.GetNames(ids)); |
| 534 | + }); |
| 535 | + |
| 536 | + var passes = ExecuteCountingPasses(schema, sp, "{ apiKeys { key customerId } }"); |
| 537 | + |
| 538 | + Assert.Equal([true], passes); |
| 539 | + Assert.Equal(0, names.CallCount); |
| 540 | + } |
| 541 | + |
| 542 | + /// <summary> |
| 543 | + /// Selecting the bulk field opts the root service list into the two passes - one load for all rows. |
| 544 | + /// </summary> |
| 545 | + [Fact] |
| 546 | + public void ServiceRootList_WithBulkFieldSelected_RunsTwoPasses() |
| 547 | + { |
| 548 | + var (schema, sp, _, names, _) = BuildApiKeysSchema(apiKey => |
| 549 | + { |
| 550 | + apiKey |
| 551 | + .AddField("createdByName", "Creator display name") |
| 552 | + .Resolve<CreatedByNameService>((k, svc) => svc.GetName(k.CreatedById)) |
| 553 | + .ResolveBulk<CreatedByNameService, string, string?>(k => k.CreatedById!, (ids, svc) => svc.GetNames(ids)); |
| 554 | + }); |
| 555 | + |
| 556 | + var passes = ExecuteCountingPasses(schema, sp, "{ apiKeys { key createdByName } }"); |
| 557 | + |
| 558 | + Assert.Equal([false, true], passes); |
| 559 | + Assert.Equal(1, names.CallCount); |
| 560 | + } |
| 561 | + |
| 562 | + /// <summary> |
| 563 | + /// The bulk field reached through a fragment spread counts too - clients hoist selections into fragments, |
| 564 | + /// and a shape check that did not look inside them would quietly drop back to one call per row. |
| 565 | + /// </summary> |
| 566 | + [Fact] |
| 567 | + public void ServiceRootList_BulkFieldInFragment_StillLoadsOnce() |
| 568 | + { |
| 569 | + var (schema, sp, _, names, _) = BuildApiKeysSchema(apiKey => |
| 570 | + { |
| 571 | + apiKey |
| 572 | + .AddField("createdByName", "Creator display name") |
| 573 | + .Resolve<CreatedByNameService>((k, svc) => svc.GetName(k.CreatedById)) |
| 574 | + .ResolveBulk<CreatedByNameService, string, string?>(k => k.CreatedById!, (ids, svc) => svc.GetNames(ids)); |
| 575 | + }); |
| 576 | + |
| 577 | + var res = schema.ExecuteRequest( |
| 578 | + new QueryRequest { Query = "query { apiKeys { ...KeyFields } } fragment KeyFields on ApiKey { key createdByName }" }, |
| 579 | + sp, |
| 580 | + null |
| 581 | + ); |
| 582 | + |
| 583 | + if (res.Errors != null) |
| 584 | + Assert.Fail(string.Join(" | ", res.Errors.Select(e => e.Message))); |
| 585 | + Assert.Equal(1, names.CallCount); |
| 586 | + dynamic keys = res.Data!["apiKeys"]!; |
| 587 | + Assert.Equal("Alice", keys[0].createdByName); |
| 588 | + } |
| 589 | + |
| 590 | + /// <summary>The isFinal flag of each execution the engine runs for a query.</summary> |
| 591 | + private static List<bool> ExecuteCountingPasses(SchemaProvider<EmptyContext> schema, ServiceProvider sp, string query) |
| 592 | + { |
| 593 | + List<bool> passes = []; |
| 594 | + var res = schema.ExecuteRequest( |
| 595 | + new QueryRequest { Query = query }, |
| 596 | + sp, |
| 597 | + null, |
| 598 | + new ExecutionOptions |
| 599 | + { |
| 600 | + BeforeExecuting = (Expression expression, bool isFinal) => |
| 601 | + { |
| 602 | + passes.Add(isFinal); |
| 603 | + return expression; |
| 604 | + }, |
| 605 | + } |
| 606 | + ); |
| 607 | + if (res.Errors != null) |
| 608 | + Assert.Fail(string.Join(" | ", res.Errors.Select(e => e.Message))); |
| 609 | + return passes; |
| 610 | + } |
| 611 | + |
519 | 612 | } |
0 commit comments