Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions website/src/docs/fusion/v16/data-requirements-and-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,20 @@ type Product {
}
```

**C# resolver**

```csharp
[ObjectType<Product>]
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);
Comment on lines +224 to +228
}
```

The gateway traverses `seller.address.countryCode` on the entity and passes the resolved value as the `countryCode` argument.

### List Aggregation Paths
Expand All @@ -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<Product>]
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);
Comment on lines +258 to +262
}
```

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)
Comment on lines +266 to +271
=> 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.
Expand Down
98 changes: 93 additions & 5 deletions website/src/docs/fusion/v16/entities-and-lookups.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Product?> GetProductByTenantAndSkuAsync(
int tenantId,
string sku,
IProductRepository repository,
CancellationToken cancellationToken)
=> repository.GetByTenantAndSkuAsync(tenantId, sku, cancellationToken);
}
```

Expand Down Expand Up @@ -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<User?> 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.
Expand All @@ -260,10 +287,12 @@ type Query {
public static partial class ProductQueries
{
[Lookup]
public static Product? GetProductByTenantAndSku(
public static Task<Product?> GetProductByTenantAndSkuAsync(
int tenantId,
string sku)
=> ProductRepository.GetByTenantAndSku(tenantId, sku);
string sku,
IProductRepository repository,
CancellationToken cancellationToken)
=> repository.GetByTenantAndSkuAsync(tenantId, sku, cancellationToken);
}
```

Expand Down Expand Up @@ -292,6 +321,22 @@ type Query {
}
```

**C# resolver**

```csharp
[QueryType]
public static partial class ProductQueries
{
[Lookup]
public static Task<Product?> 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
Expand All @@ -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<Product?> 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).
Expand Down Expand Up @@ -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<Product>] int Id,
string? Sku,
string? Category);
```

**GraphQL schema with nested composite key**

```graphql
Expand All @@ -377,6 +450,21 @@ type Tenant {
}
```

**C# type declaration**

```csharp
[EntityKey("id")]
[EntityKey("sku tenant { id }")]
public sealed record Product(
[property: ID<Product>] int Id,
string? Sku,
Tenant? Tenant);

public sealed record Tenant([property: ID<Tenant>] 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.
Expand Down