diff --git a/website/src/docs/fusion/v16/data-requirements-and-mapping.md b/website/src/docs/fusion/v16/data-requirements-and-mapping.md index 10b2159178e..d860392deb7 100644 --- a/website/src/docs/fusion/v16/data-requirements-and-mapping.md +++ b/website/src/docs/fusion/v16/data-requirements-and-mapping.md @@ -215,6 +215,20 @@ type Product { } ``` +**C# resolver** + +```csharp +[ObjectType] +public static partial class ProductNode +{ + public static decimal GetTaxEstimate( + [Parent] Product product, + [Require("seller.address.countryCode")] string countryCode, + [Require] float price) + => TaxCalculator.Estimate(countryCode, price); +} +``` + The gateway traverses `seller.address.countryCode` on the entity and passes the resolved value as the `countryCode` argument. ### List Aggregation Paths @@ -235,6 +249,29 @@ type Product { The path `seller.addresses[countryCode]` means: navigate to `seller.addresses` (a list), then select `countryCode` from each element. If the seller has three addresses with country codes `"US"`, `"DE"`, and `"US"`, the resolver receives `["US", "DE", "US"]` as the `countryCodes` argument. +**C# resolver** + +```csharp +[ObjectType] +public static partial class ProductNode +{ + public static decimal GetTaxEstimate( + [Parent] Product product, + [Require("seller.addresses[countryCode]")] string[] countryCodes, + [Require] float price) + => TaxCalculator.Estimate(countryCodes, price); +} +``` + +For the list projection variant (`dimensions[{ weight, height }]`), the argument is a list of an input object type: + +```csharp +public static int GetBulkEstimate( + [Parent] Product product, + [Require("dimensions[{ weight, height }]")] ProductDimensionInput[] dimensions) + => ShippingCalculator.Bulk(dimensions); +``` + ## Declaring Contextually Available Fields Use `@provides` on a field that returns an entity to tell the gateway that certain subfields of that entity are available when resolved through this specific field. The subgraph does not own those fields globally, but it can provide them in this context. diff --git a/website/src/docs/fusion/v16/entities-and-lookups.md b/website/src/docs/fusion/v16/entities-and-lookups.md index fc7fa73aae0..ccccfe3392c 100644 --- a/website/src/docs/fusion/v16/entities-and-lookups.md +++ b/website/src/docs/fusion/v16/entities-and-lookups.md @@ -157,8 +157,12 @@ public static partial class Query public partial class InternalLookups { [Lookup] - public Product? GetProductByTenantAndSku(int tenantId, string sku) - => ProductRepository.GetByTenantAndSku(tenantId, sku); + public Task GetProductByTenantAndSkuAsync( + int tenantId, + string sku, + IProductRepository repository, + CancellationToken cancellationToken) + => repository.GetByTenantAndSkuAsync(tenantId, sku, cancellationToken); } ``` @@ -234,6 +238,29 @@ input UserByInput @oneOf { In this case we use the `@is` directive with the choice operator `|` to signal to Fusion that it can use this lookup either with the `id` or the `username` as a key. +**C# resolver** + +```csharp +[OneOf] +public sealed record UserByInput(int? Id, string? Username); + +[QueryType] +public static partial class UserQueries +{ + [Lookup] + public static async Task GetUser( + [Is("{ id } | { username }")] UserByInput by, + IUserByIdDataLoader userById, + IUserByNameDataLoader userByName, + CancellationToken cancellationToken) + => by.Id is { } id + ? await userById.LoadAsync(id, cancellationToken) + : await userByName.LoadAsync(by.Username!, cancellationToken); +} +``` + +The `[OneOf]` attribute on the input class emits the `@oneOf` directive in the schema. The `[Is(...)]` attribute on the parameter carries the FieldSelectionMap that tells Fusion this lookup accepts either `id` or `username` as the key. + ### Composite Keys Some entities are identified by a combination of fields instead of a single field. In that case, the lookup arguments together form the key. @@ -260,10 +287,12 @@ type Query { public static partial class ProductQueries { [Lookup] - public static Product? GetProductByTenantAndSku( + public static Task GetProductByTenantAndSkuAsync( int tenantId, - string sku) - => ProductRepository.GetByTenantAndSku(tenantId, sku); + string sku, + IProductRepository repository, + CancellationToken cancellationToken) + => repository.GetByTenantAndSkuAsync(tenantId, sku, cancellationToken); } ``` @@ -292,6 +321,22 @@ type Query { } ``` +**C# resolver** + +```csharp +[QueryType] +public static partial class ProductQueries +{ + [Lookup] + public static Task GetProductAsync( + [Is("tenant.id")] int tenantId, + [Is("sku")] string sku, + IProductRepository repository, + CancellationToken cancellationToken) + => repository.GetByTenantAndSkuAsync(tenantId, sku, cancellationToken); +} +``` + **GraphQL schema with input-object mapping** ```graphql @@ -317,6 +362,23 @@ type Query { } ``` +**C# resolver** + +```csharp +public sealed record ProductKeyInput(int TenantId, string Sku); + +[QueryType] +public static partial class ProductQueries +{ + [Lookup] + public static Task GetProductAsync( + [Is("{ tenantId: tenant.id, sku }")] ProductKeyInput key, + IProductRepository repository, + CancellationToken cancellationToken) + => repository.GetByTenantAndSkuAsync(key.TenantId, key.Sku, cancellationToken); +} +``` + Both variants describe the same composite key. The first maps each argument explicitly. The second maps the input object fields in one selection map. > The FieldSelectionMap syntax from the Composite Schemas specification supports more advanced argument-to-field mappings for lookups. For the full grammar and examples, see the [Composite Schemas specification](https://graphql.github.io/composite-schemas-spec/draft/#sec-Appendix-A-Specification-of-FieldSelectionMap-Scalar). @@ -363,6 +425,17 @@ type Product @key(fields: "id") @key(fields: "sku category") { } ``` +**C# type declaration** + +```csharp +[EntityKey("id")] +[EntityKey("sku category")] +public sealed record Product( + [property: ID] int Id, + string? Sku, + string? Category); +``` + **GraphQL schema with nested composite key** ```graphql @@ -377,6 +450,21 @@ type Tenant { } ``` +**C# type declaration** + +```csharp +[EntityKey("id")] +[EntityKey("sku tenant { id }")] +public sealed record Product( + [property: ID] int Id, + string? Sku, + Tenant? Tenant); + +public sealed record Tenant([property: ID] int Id); +``` + +The `[EntityKey]` attribute uses GraphQL field names. Stack multiple attributes on the type to declare multiple keys, and use the same nested-selection syntax (`tenant { id }`) you would write in SDL. + ## GraphQL Global Object Identification If your subgraphs implement GraphQL Global Object Identification, with a `node` field on `Query` and a `Node` interface, you already have a strong entity identity contract. You can build on this by using `node` as a lookup and treating types that implement `Node` as entities.