diff --git a/website/src/docs/docs.json b/website/src/docs/docs.json index 7d22c543fa1..dc0b4737a7f 100644 --- a/website/src/docs/docs.json +++ b/website/src/docs/docs.json @@ -155,6 +155,340 @@ "metaDescription": "Hot Chocolate is the most efficient, feature-rich, open-source GraphQL server in the .NET ecosystem, that helps developers to build powerful APIs.", "latestStableVersion": "v14", "versions": [ + { + "path": "v15", + "title": "v15", + "items": [ + { + "path": "index", + "title": "Introduction" + }, + { + "path": "get-started-with-graphql-in-net-core", + "title": "Getting Started" + }, + { + "path": "defining-a-schema", + "title": "Defining a schema", + "items": [ + { + "path": "index", + "title": "Overview" + }, + { + "path": "queries", + "title": "Queries" + }, + { + "path": "mutations", + "title": "Mutations" + }, + { + "path": "subscriptions", + "title": "Subscriptions" + }, + { + "path": "object-types", + "title": "Object Types" + }, + { + "path": "scalars", + "title": "Scalars" + }, + { + "path": "arguments", + "title": "Arguments" + }, + { + "path": "input-object-types", + "title": "Input Object Types" + }, + { + "path": "lists", + "title": "Lists" + }, + { + "path": "non-null", + "title": "Non-Null" + }, + { + "path": "enums", + "title": "Enums" + }, + { + "path": "interfaces", + "title": "Interfaces" + }, + { + "path": "unions", + "title": "Unions" + }, + { + "path": "extending-types", + "title": "Extending Types" + }, + { + "path": "directives", + "title": "Directives" + }, + { + "path": "documentation", + "title": "Documentation" + }, + { + "path": "versioning", + "title": "Versioning" + }, + { + "path": "relay", + "title": "Relay" + }, + { + "path": "dynamic-schemas", + "title": "Dynamic Schemas" + } + ] + }, + { + "path": "fetching-data", + "title": "Fetching data", + "items": [ + { + "path": "index", + "title": "Overview" + }, + { + "path": "resolvers", + "title": "Resolvers" + }, + { + "path": "fetching-from-databases", + "title": "Fetching from Databases" + }, + { + "path": "fetching-from-rest", + "title": "Fetching from REST" + }, + { + "path": "dataloader", + "title": "DataLoader" + }, + { + "path": "pagination", + "title": "Pagination" + }, + { + "path": "filtering", + "title": "Filtering" + }, + { + "path": "sorting", + "title": "Sorting" + }, + { + "path": "projections", + "title": "Projections" + } + ] + }, + { + "path": "execution-engine", + "title": "Execution Engine", + "items": [ + { + "path": "index", + "title": "Overview" + }, + { + "path": "field-middleware", + "title": "Field middleware" + } + ] + }, + { + "path": "integrations", + "title": "Integrations", + "items": [ + { + "path": "index", + "title": "Overview" + }, + { + "path": "entity-framework", + "title": "Entity Framework" + }, + { + "path": "mongodb", + "title": "MongoDB" + }, + { + "path": "spatial-data", + "title": "Spatial Data" + }, + { + "path": "marten", + "title": "Marten" + } + ] + }, + { + "path": "server", + "title": "Server", + "items": [ + { + "path": "index", + "title": "Overview" + }, + { + "path": "endpoints", + "title": "Endpoints" + }, + { + "path": "http-transport", + "title": "HTTP transport" + }, + { + "path": "interceptors", + "title": "Interceptors" + }, + { + "path": "dependency-injection", + "title": "Dependency injection" + }, + { + "path": "global-state", + "title": "Global State" + }, + { + "path": "introspection", + "title": "Introspection" + }, + { + "path": "files", + "title": "Files" + }, + { + "path": "instrumentation", + "title": "Instrumentation" + }, + { + "path": "batching", + "title": "Batching" + }, + { + "path": "command-line", + "title": "Command Line" + } + ] + }, + { + "path": "fusion", + "title": "Fusion", + "items": [ + { + "path": "index", + "title": "Overview" + } + ] + }, + { + "path": "performance", + "title": "Performance", + "items": [ + { + "path": "index", + "title": "Overview" + }, + { + "path": "persisted-operations", + "title": "Persisted operations" + }, + { + "path": "automatic-persisted-operations", + "title": "Automatic persisted operations" + } + ] + }, + { + "path": "security", + "title": "Security", + "items": [ + { + "path": "index", + "title": "Overview" + }, + { + "path": "authentication", + "title": "Authentication" + }, + { + "path": "authorization", + "title": "Authorization" + }, + { + "path": "cost-analysis", + "title": "Cost Analysis" + } + ] + }, + { + "path": "api-reference", + "title": "API Reference", + "items": [ + { + "path": "custom-attributes", + "title": "Custom Attributes" + }, + { + "path": "errors", + "title": "Errors" + }, + { + "path": "language", + "title": "Language" + }, + { + "path": "extending-filtering", + "title": "Extending Filtering" + }, + { + "path": "visitors", + "title": "Visitors" + }, + { + "path": "apollo-federation", + "title": "Apollo Federation" + }, + { + "path": "executable", + "title": "Executable" + } + ] + }, + { + "path": "migrating", + "title": "Migrating", + "items": [ + { + "path": "migrate-from-13-to-14", + "title": "Migrate from 13 to 14" + }, + { + "path": "migrate-from-12-to-13", + "title": "Migrate from 12 to 13" + }, + { + "path": "migrate-from-11-to-12", + "title": "Migrate from 11 to 12" + }, + { + "path": "migrate-from-10-to-11", + "title": "Migrate from 10 to 11" + } + ] + } + ] + }, { "path": "v14", "title": "v14", @@ -1658,6 +1992,106 @@ "metaDescription": "Strawberry Shake is an incredible GraphQL client for the .NET ecosystem, that helps developers to build awesome UIs in Blazor, Maui, and more.", "latestStableVersion": "v14", "versions": [ + { + "path": "v15", + "title": "v15", + "items": [ + { + "path": "index", + "title": "Introduction" + }, + { + "path": "get-started", + "title": "Get Started", + "items": [ + { + "path": "index", + "title": "Blazor" + }, + { + "path": "xamarin", + "title": "Xamarin" + }, + { + "path": "console", + "title": "Console" + } + ] + }, + { + "path": "subscriptions", + "title": "Subscriptions" + }, + { + "path": "tooling", + "title": "Tooling / CLI" + }, + { + "path": "caching", + "title": "Caching", + "items": [ + { + "path": "index", + "title": "Overview" + }, + { + "path": "entities", + "title": "Entities" + }, + { + "path": "invalidation", + "title": "Invalidation" + } + ] + }, + { + "path": "performance", + "title": "Performance", + "items": [ + { + "path": "index", + "title": "Overview" + }, + { + "path": "persisted-operations", + "title": "Persisted Operations" + }, + { + "path": "persisted-state", + "title": "Persisted State" + } + ] + }, + { + "path": "networking", + "title": "Networking", + "items": [ + { + "path": "index", + "title": "Protocols" + }, + { + "path": "authentication", + "title": "Authentication" + } + ] + }, + { + "path": "scalars", + "title": "Scalars" + }, + { + "path": "migrating", + "title": "Migrating", + "items": [ + { + "path": "migrate-from-12-to-13", + "title": "Migrate from 12 to 13" + } + ] + } + ] + }, { "path": "v14", "title": "v14", diff --git a/website/src/docs/hotchocolate/v15/api-reference/apollo-federation.md b/website/src/docs/hotchocolate/v15/api-reference/apollo-federation.md new file mode 100644 index 00000000000..8e410fe67f7 --- /dev/null +++ b/website/src/docs/hotchocolate/v15/api-reference/apollo-federation.md @@ -0,0 +1,716 @@ +--- +title: Apollo Federation Subgraph Support +--- + +> If you want to read more about Apollo Federation in general, you can head over to the [Apollo Federation documentation](https://www.apollographql.com/docs/federation/), which provides a robust overview and set of examples for this GraphQL architectural pattern. Many of the core principles and concepts are referenced within this document. + +Hot Chocolate includes an implementation of the Apollo Federation v1 specification for creating Apollo Federated subgraphs. Through Apollo Federation, you can combine multiple GraphQL APIs into a single API for your consumers. + +The documentation describes the syntax for creating an Apollo Federated subgraph using Hot Chocolate and relates the implementation specifics to its counterpart in the Apollo Federation docs. This document _will not_ provide a thorough explanation of the Apollo Federation core concepts nor will it describe how you go about creating a supergraph to stitch together various subgraphs, as the Apollo Federation team already provides thorough documentation of those principles. + +You can find example projects of the Apollo Federation library in [Hot Chocolate examples](https://github.com/ChilliCream/graphql-platform/tree/main/src/HotChocolate/ApolloFederation/examples). + +# Get Started + +To use the Apollo Federation tools, you need to first install v12.6 or later of the `HotChocolate.ApolloFederation` package. + + + +After installing the necessary package, you'll need to register the Apollo Federation services with the GraphQL server. + +```csharp +builder.Services + .AddGraphQLServer() + .AddApolloFederation(); +``` + +# Defining an entity + +Now that the API is ready to support Apollo Federation, we'll need to define an **entity**—an object type that can resolve its fields across multiple subgraphs. We'll work with a `Product` entity to provide an example of how to do this. + + + + +```csharp +public class Product +{ + [ID] + public string Id { get; set; } + + public string Name { get; set; } + + public float Price { get; set; } +} +``` + + + + + +```csharp +public class Product +{ + public string Id { get; set; } + + public string Name { get; set; } + + public float Price { get; set; } +} + +public class ProductType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(product => product.Id).ID(); + } +} +``` + + + + + +**Coming soon** + + + + +## Define an entity key + +Once we have an object type to work with, we'll [define a key](https://www.apollographql.com/docs/federation/entities#1-define-a-key) for the entity. A key in an Apollo Federated subgraph effectively serves as an "identifier" that can uniquely locate an individual record of that type. This will typically be something like a record's primary key, a SKU, or an account number. + + + + + +In an implementation-first approach, we'll use the `[Key]` attribute on any property or properties that can be referenced as a key by another subgraph. + +```csharp +public class Product +{ + [ID] + [Key] + public string Id { get; set; } + + public string Name { get; set; } + + public float Price { get; set; } +} +``` + + + + + +In a code-first approach, we'll use the `Key()` method to designate any GraphQL fields that can be reference as a key by another subgraph. + +```csharp +public class Product +{ + public string Id { get; set; } + + public string Name { get; set; } + + public float Price { get; set; } +} + +public class ProductType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(product => product.Id).ID(); + + // Matches the Id property when it is converted to the GraphQL schema + descriptor.Key("id"); + } +} +``` + + + + + +**Coming soon** + + + + + +## Define a reference resolver + +Next, we'll need to define an [entity reference resolver](https://www.apollographql.com/docs/federation/entities#2-define-a-reference-resolver) so that the supergraph can resolve this entity across multiple subgraphs during a query. Every subgraph that contributes at least one unique field to an entity must define a reference resolver for that entity. + + + + + +In an implementation-first approach, a reference resolver will work just like a [regular resolver](/docs/hotchocolate/v15/fetching-data/resolvers) with some key differences: + +1. It must be annotated with the `[ReferenceResolver]` attribute +1. It must be a `public static` method _within_ the type it is resolving + +```csharp +public class Product +{ + [ID] + [Key] + public string Id { get; set; } + + public string Name { get; set; } + + public float Price { get; set; } + + [ReferenceResolver] + public static async Task ResolveReference( + // Represents the value that would be in the Id property of a Product + string id, + // Example of a service that can resolve the Products + ProductBatchDataLoader dataLoader + ) + { + return await dataloader.LoadAsync(id); + } +} +``` + +Some important details to highlight about `[ReferenceResolver]` methods. + +1. The name of the method decorated with the `[ReferenceResolver]` attribute does not matter. However, as with all programming endeavors, you should aim to provide a descriptive name that reveals the method's intention. +1. The parameter name and type used in the reference resolver **must match** the GraphQL field name of the `[Key]` attribute, e.g., if the GraphQL key field is `id: String!` or `id: ID!` then the reference resolver's parameter must be `string id`. +1. If you're using [nullable reference types](https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references), you should make sure the return type is marked as possibly null, i.e., `T?`. +1. If you have multiple keys defined for an entity, you should include a reference resolver for _each key_ so that the supergraph is able to resolve your entity regardless of which key(s) another graph uses to reference that entity. + +```csharp +public class Product +{ + [Key] + public string Id { get; set; } + + [Key] + public int Sku { get; set; } + + [ReferenceResolver] + public static Product? ResolveReferenceById(string id) + { + // Locates the Product by its Id. + } + + [ReferenceResolver] + public static Product? ResolveReferenceBySku(int sku) + { + // Locates the product by SKU + } +} +``` + + + + + +We'll now chain a `ResolveReferenceWith()` method call off of the `Key()` method call from the previous step. This will create a [resolver](/docs/hotchocolate/v15/fetching-data/resolvers) that the Hot Chocolate engine can invoke. + +```csharp +public class Product +{ + public string Id { get; set; } + + public string Name { get; set; } + + public float Price { get; set; } +} + +public class ProductType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(product => product.Id).ID(); + + descriptor.Key("id") + .ResolveReferenceWith(_ => ResolveByIdAsync(default!, default!)); + } + + private static Task ResolveByIdAsync( + // Represents the value that would be in the Id property of a Product + string id, + // Example of a service that can resolve the Products + ProductBatchDataLoader dataLoader) + { + return await dataLoader.LoadAsync(id); + } +} +``` + +Some important details to highlight about entity reference resolvers. + +1. The parameter name and type used in the reference resolver **must match** the GraphQL field name of the `Key()` field set, e.g., if the GraphQL key field is `id: String!` or `id: ID!` then the reference resolver's parameter must be `string id`. +1. If you're using [nullable reference types](https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references), you should make sure the return type is marked as possibly null, i.e., `T?`. +1. For each call to the `Key()` method, you should include a reference resolver so that the supergraph is able to resolve your entity regardless of which key(s) another graph uses to reference that entity. + +```csharp +public class ProductType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Key("id") + .ResolveReferenceWith(_ => ResolveByIdAsync(default!)); + + descriptor.Key("sku") + .ResolveReferenceWith(_ => ResolveBySkuAsync(default!)) + } + + private static Task ResolveByIdAsync(string id) + { + // Locate the product by its Id + } + + private static Task ResolveBySkuAsync(default!) + { + // Locate the product by its SKU instead + } +} +``` + + + + + +**Coming soon** + + + + +> ### A note about reference resolvers +> +> It's recommended to use a [dataloader](/docs/hotchocolate/v15/fetching-data/dataloader) to fetch the data in a reference resolver. This helps the API avoid [an N+1 problem](https://www.apollographql.com/docs/federation/entities-advanced#handling-the-n1-problem) when a query resolves multiple items from a given subgraph. + +## Register the entity + +After our type has a key or keys and a reference resolver defined, you'll register the type in the GraphQL schema, which will register it as a type within the GraphQL API itself as well as within the [auto-generated `_service { sdl }` field](https://www.apollographql.com/docs/federation/subgraph-spec/#required-resolvers-for-introspection) within the API. + +_Entity type registration_ + + + + + +```csharp +builder.Services + .AddGraphQLServer() + .AddApolloFederation() + .AddType() + // other registrations... + ; +``` + + + + + +```csharp +builder.Services + .AddGraphQLServer() + .AddApolloFederation() + .AddType() + // other registrations... + ; +``` + + + + + +**Coming soon** + + + + +## Testing and executing your reference resolvers + +After creating an entity, you'll likely wonder "how do I invoke and test this reference resolver?" Entities that define a reference resolver can be queried through the [auto-generated `_entities` query](https://www.apollographql.com/docs/federation/subgraph-spec#understanding-query_entities) at the subgraph level. + +You'll invoke the query by providing an array of representations using a combination of a `__typename` and key field values to invoke the appropriate resolver. An example query for our `Product` would look something like the following. + +_Entities query_ + +```graphql +query { + _entities( + representations: [ + { __typename: "Product", id: "" } + # You can provide multiple representations for multiple objects and types in the same query + ] + ) { + ... on Product { + id + name + price + } + } +} +``` + +_Entities query result_ + +```json +{ + "data": { + "_entities": [ + { + "id": "", + "name": "Foobar", + "price": 10.99 + } + // Any other values that were found, or null + ] + } +} +``` + +> **Note**: The `_entities` field is an internal implementation detail of Apollo Federation that is necessary for the supergraph to properly resolve entities. API consumers **should not** use the `_entities` field directly nor should they send requests to a subgraph directly. We're only highlighting how to use the `_entities` field so that you can validate and test your subgraph and its entity reference resolvers at runtime or using tools like [`Microsoft.AspNetCore.Mvc.Testing`](https://learn.microsoft.com/aspnet/core/test/integration-tests). + +# Referencing an entity type + +Now that we have an entity defined in one of our subgraphs, let's go ahead and create a second subgraph that will make use of our `Product` type. Remember, all of this work should be performed in a _**separate API project**_. + +In the second subgraph, we'll create a `Review` type that is focused on providing reviews of `Product` entities from the other subgraph. We'll do that by defining our `Review` type along with a [service type reference](https://www.apollographql.com/docs/federation/entities/#referencing-an-entity-without-contributing-fields) that represents the `Product`. + +In our new subgraph API we'll need to start by creating the `Product`. When creating the extended service type, make sure to consider the following details + +- The _GraphQL type name_ **must match**. Often, this can be accomplished by using the same class name between the projects, but you can also use tools like the `[GraphQLName(string)]` attribute or `IObjectTypeDescriptor.Name(string)` method to explicitly set a GraphQL name. +- The extended type must include _at least one_ key that matches in both name and GraphQL type from the source graph. + - In our example, we'll be referencing the `id: ID!` field that was defined on our `Product` + + + + + +```csharp +[ExtendServiceType] +public class Product +{ + [ID] + [Key] + public string Id { get; set; } +} + +// In your Program +builder.Services + .AddGraphQLServer() + .AddApolloFederation() + .AddType(); +``` + + + + + +```csharp +public class Product +{ + public string Id { get; set; } +} + +public class ProductType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.ExtendServiceType(); + + descriptor.Key("id"); + descriptor.Field(product => product.Id).ID(); + } +} + +// In your Program +builder.Services + .AddGraphQLServer() + .AddApolloFederation() + .AddType(); +``` + + + + + +**Coming soon** + + + + + +Next, we'll create our `Review` type that has a reference to the `Product` entity. Similar to our first class, we'll need to denote the type's key(s) and the corresponding entity reference resolver(s). + + + + + +```csharp +public class Review +{ + [ID] + [Key] + public string Id { get; set; } + + public string Content { get; set; } + + [GraphQLIgnore] + public string ProductId { get; set; } + + public Product GetProduct() => new Product { Id = ProductId }; + + [ReferenceResolver] + public static Review? ResolveReference(string id) + { + // Omitted for brevity; some kind of service to retrieve the review. + } +} + +// In your Program +builder.Services + .AddGraphQLServer() + .AddApolloFederation() + .AddType() + .AddType(); +``` + + + + + +```csharp +public class Review +{ + public string Id { get; set; } + + public string Content { get; set; } + + public string ProductId { get; set; } + + public Product GetProduct() => new Product { Id = ProductId }; +} + +public class ReviewType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Key("id").ResolveReferenceWith(_ => ResolveReviewById(default!)); + descriptor.Field(review => review.Id).ID(); + + descriptor.Ignore(review => review.ProductId); + } + + private static Review? ResolveReviewById(string id) + { + // Omitted for brevity + } +} + +// In your Program +builder.Services + .AddGraphQLServer() + .AddApolloFederation() + .AddType() + .AddType(); +``` + + + + + +**Coming soon** + + + + + +In the above snippet two things may pop out as strange to you: + +1. Why did we explicitly ignore the `ProductId` property? + - The `ProductId` is, in essence, a "foreign key" to the other graph. Instead of presenting that data as a field of the `Review` type, we're presenting it through the `product: Product!` GraphQL field that is produced by the `GetProduct()` method. This allows the Apollo supergraph to stitch the `Review` and `Product` types together and represent that a query can traverse from the `Review` to the `Product` it is reviewing and make the API more graph-like. With that said, it is not strictly necessary to ignore the `ProductId` or any other external entity Id property. +2. Why does the `GetProduct()` method instantiate its own `new Product { Id = ProductId }` object? + - Since our goal with Apollo Federation is decomposition and [concern-based separation](https://www.apollographql.com/docs/federation/#concern-based-separation), a second subgraph is likely to have that "foreign key" reference to the type that is reference from the other subgraph. However, this graph does not "own" the actual data of the entity itself. This is why our sample simply performs a `new Product { Id = ProductId }` statement for the resolver: it's not opinionated about how the other data of a `Product` is resolved from its owning graph. + +With our above changes, we can successfully connect these two subgraphs into a single query within an Apollo supergraph, allowing our API users to send a query like the following. + +```graphql +query { + # Example - not explicitly defined in our tutorial + review(id: "") { + id + content + product { + id + name + } + } +} +``` + +As a reminder, you can create and configure a supergraph by following either the [Apollo Router documentation](https://www.apollographql.com/docs/router/quickstart/) or [`@apollo/gateway` documentation](https://www.npmjs.com/package/@apollo/gateway). + +## Contributing fields through resolvers + +Now that our new subgraph has the `Product` reference we can [contribute additional fields to the type](https://www.apollographql.com/docs/federation/entities#contributing-entity-fields). Similar to other types in Hot Chocolate, you can create new fields by defining different method or property resolvers. For a full set of details and examples on creating resolvers, you can read our [documentation on resolvers](/docs/hotchocolate/v15/fetching-data/resolvers). + +For now, we'll focus on giving our supergraph the ability to retrieve all reviews for a given product by adding a `reviews: [Review!]!` property to the type. + + + + + +```csharp +[ExtendServiceType] +public class Product +{ + [ID] + [Key] + public string Id { get; set; } + + public async Task> GetReviews( + ReviewRepository repo // example of how you might resolve this data + ) + { + return await repo.GetReviewsByProductIdAsync(Id); + } +} +``` + + + + + +```csharp +public class Product +{ + public string Id { get; set; } + + public async Task> GetReviews( + ReviewRepository repo // example of how you might resolve this data + ) + { + return await repo.GetReviewsByProductIdAsync(Id); + } +} + +public class ProductType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.ExtendServiceType(); + + descriptor.Key("id"); + descriptor.Field(product => product.Id).ID(); + } +} +``` + + + + + +**Coming soon** + + + + + +These changes will successfully add the new field within the subgraph! However, our current implementation cannot be resolved if we start at a product such as `query { product(id: "foo") { reviews { ... } } }`. To fix this, we'll need to implement an entity reference resolver in our second subgraph. + +As mentioned above, since this subgraph does not "own" the data for a `Product`, our resolver will be fairly naive, similar to the `Review::GetProduct()` method: it will simply instantiate a `new Product { Id = id }`. We do this because the reference resolver should only be directly invoked by the supergraph, so our new reference resolver will simply assume the data exists. However, if there is data that needs to be fetched from some kind of data store, the resolver can still do this just as any other data resolver in Hot Chocolate. + + + + + +```csharp +[ExtendServiceType] +public class Product +{ + [ID] + [Key] + public string Id { get; set; } + + public async Task> GetReviews( + ReviewRepository repo // example of how you might resolve this data + ) + { + return await repo.GetReviewsByProductIdAsync(Id); + } + + [ReferenceResolver] + public static Product ResolveProductReference(string id) => new Product { Id = id }; +} +``` + + + + + +```csharp +public class Product +{ + public string Id { get; set; } + + public async Task> GetReviews( + ReviewRepository repo // example of how you might resolve this data + ) + { + return await repo.GetReviewsByProductIdAsync(Id); + } +} + +public class ProductType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.ExtendServiceType(); + + descriptor.Key("id").ResolveReferenceWith(_ => ResolveProductReference(default!)); + descriptor.Field(product => product.Id).ID(); + } + + private static Product ResolveProductReference(string id) => new Product { Id = id }; +} +``` + + + + + +**Coming soon** + + + + + +With the above changes, our supergraph can now support traversing both "from a review to a product" as well as "from a product to a review"! + +```graphql +# Example root query fields - not implemented in the tutorial +query { + # From a review to a product (back to the reviews) + review(id: "foo") { + id + content + product { + id + name + price + reviews { + id + content + } + } + } + # From a product to a review + product(id: "bar") { + id + name + price + reviews { + id + content + } + } +} +``` diff --git a/website/src/docs/hotchocolate/v15/api-reference/custom-attributes.md b/website/src/docs/hotchocolate/v15/api-reference/custom-attributes.md new file mode 100644 index 00000000000..98878bf95c0 --- /dev/null +++ b/website/src/docs/hotchocolate/v15/api-reference/custom-attributes.md @@ -0,0 +1,192 @@ +--- +title: "Custom Attributes" +--- + +Hot Chocolate allows to define a schema in various ways. When defining schemas with pure .NET types and custom attributes we need a way to access advanced features like custom field middleware that we have at our disposal with schema types. + +```csharp +public class QueryType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Strings).UsePaging(); + } +} +``` + +This is where descriptor attributes come in. Descriptor attributes allow us to package descriptor configurations into an attribute that can be used to decorate our .NET types. Descriptor attributes act like an interceptor into the configuration of the inferred schema type. + +# Built-In Attributes + +We have prepared the following set of built-in descriptor attributes. + +> ⚠️ **Note:** As middleware comprises the stages of a sequential _pipeline_, the ordering is important. The correct order to use is `UsePaging`, `UseFiltering`, `UseSorting`. + +## UsePagingAttribute + +The `UsePagingAttribute` allows us to use the paging middleware by annotating it to a property or method. + +```csharp +public class Query +{ + [UsePaging] + public IQueryable GetFoos() + { + ... + } +} +``` + +## UseFilteringAttribute + +The `UseFilteringAttribute` allows us to apply the filtering middleware to a property or method. + +```csharp +public class Query +{ + [UseFiltering] + public IQueryable GetFoos() + { + ... + } +} +``` + +> Warning: Be sure to install the `HotChocolate.Types.Filters` NuGet package. + +## UseSortingAttribute + +The `UseSortingAttribute` allows us to apply the sorting middleware to a property or method. + +```csharp +public class Query +{ + [UseSorting] + public IQueryable GetFoos() + { + ... + } +} +``` + +> Warning: Be sure to install the `HotChocolate.Types.Sorting` NuGet package. + +## AuthorizeAttribute + +The `AuthorizeAttribute` allows to apply the authorize directives to a class, struct, interface, property or method. The attribute will only be applied if the inferred type is an object type. + +```csharp +public class Query +{ + [Authorize(Policy = "MyPolicy")] + public IQueryable GetFoos() + { + ... + } +} +``` + +# Attribute Chaining + +Attributes can by default be chained, meaning that the attributes are applied in order from the top one to the bottom one. + +The following code ... + +```csharp +public class Query +{ + [UsePaging] + [UseFiltering] + [UseSorting] + public IQueryable GetFoos() + { + ... + } +} +``` + +... would translate to: + +```csharp +public class QueryType + : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Foos) + .UsePaging>() + .UseFiltering() + .UseSorting(); + } +} +``` + +# Custom Descriptor Attributes + +It is super simple to create custom descriptor attributes and package complex functionality in simple to use attributes. + +```csharp +public class SomeMiddlewareAttribute + : ObjectFieldDescriptorAttribute +{ + public override void OnConfigure( + IDescriptorContext context, + IObjectFieldDescriptor descriptor, + MemberInfo member) + { + descriptor.Use(next => context => ...); + } +} +``` + +Within the `OnConfigure` method you can do what you actually would do in the `Configure` method of a type. + +But you also get some context information about where the configuration was applied to, like you get the member to which the attribute was applied to and you get the descriptor context. + +We have one descriptor base class for each first-class descriptor type. + +- EnumTypeDescriptorAttribute +- EnumValueDescriptorAttribute +- InputObjectTypeDescriptorAttribute +- InputFieldDescriptorAttribute +- InterfaceTypeDescriptorAttribute +- InterfaceFieldDescriptorAttribute +- ObjectTypeDescriptorAttribute +- ObjectFieldDescriptorAttribute +- UnionTypeDescriptorAttribute +- ArgumentDescriptorAttribute + +All of these attribute base classes have already the allowed attribute targets applied. That means that we pre-configured the `ObjectFieldDescriptorAttribute` for instance to be only valid on methods and properties. + +If you want to build more complex attributes that can be applied to multiple targets like an interface type and an object type at the same time then you can use our `DescriptorAttribute` base class. This base class is not pre-configured and lets you probe for configuration types. + +```csharp +[AttributeUsage( + AttributeTargets.Property | AttributeTargets.Method, + Inherited = true, + AllowMultiple = true)] +public sealed class MyCustomAttribute : DescriptorAttribute +{ + protected override void TryConfigure( + IDescriptorContext context, + IDescriptor descriptor, + ICustomAttributeProvider element) + { + if(element is MemberInfo member) + { + switch(descriptor) + { + case IInterfaceFieldDescriptor interfaceField: + // do something ... + break; + + case IObjectFieldDescriptor interfaceField: + // do something ... + break; + } + } + } +} +``` + +It is simple to use these attributes. Just annotating a type or a property with an attribute will add the packaged functionality. The types can be used in conjunction with schema types or without. diff --git a/website/src/docs/hotchocolate/v15/api-reference/custom-context-data.md b/website/src/docs/hotchocolate/v15/api-reference/custom-context-data.md new file mode 100644 index 00000000000..c18f5084ec1 --- /dev/null +++ b/website/src/docs/hotchocolate/v15/api-reference/custom-context-data.md @@ -0,0 +1,93 @@ +--- +title: Custom Context Data +--- + +When implementing custom middleware, it can be useful to be able to store some custom state on the context. This could be to build up a cache or other state data. Hot Chocolate has two types of context stores that we can use. + +# Global Context Data + +The global context data is a thread-safe dictionary that is available though the `IQueryContext` and the `IResolverContext`. This means we are able to share context data between query middleware components and field middleware components. + +One common use case is to aggregate some state when the GraphQL request is created and use it in field middleware or in the resolver. + +In order to intercept the request creation we can add an `IOperationRequestInterceptor` to our services and there build up our custom state. + +```csharp +services.AddQueryRequestInterceptor((ctx, builder, ct) => +{ + builder.SetProperty("Foo", new Foo()); + return Task.CompletedTask; +}); +``` + +We can access the initial provided data in a query middleware, field middleware or our resolver. + +Query Middleware Example: + +```csharp +builder.Use(next => context => +{ + // access data + var foo = (Foo)context.ContextData["Foo"]; + + // set new data + context.ContextData["Bar"] = new Bar(); + + return next.Invoke(context); +}); +``` + +Field Middleware Example: + +```csharp +SchemaBuilder.New() + .Use(next => context => + { + // access data + var foo = (Foo)context.ContextData["Foo"]; + + // set new data + context.ContextData["Bar"] = new Bar(); + + return next.Invoke(context); + }) + .Create(); +``` + +Resolver Example: + +```csharp +public Task MyResolver([State("Foo")]Foo foo) +{ + ... +} +``` + +# Scoped Context Data + +The scoped context data is a immutable dictionary and is only available through the `IResolverContext`. + +Scoped state allows us to aggregate state for our child field resolvers. + +Let's say we have the following query: + +```graphql +{ + a { + b { + c + } + } + d { + e { + f + } + } +} +``` + +If the `a`-resolver would put something on the scoped context its sub-tree could access that data. This means, `b` and `c` could access the data but `d`, `e` and `f` would _NOT_ be able to access the data, their dictionary is still unchanged. + +```csharp +context.ScopedContextData = context.ScopedContextData.SetItem("foo", "bar"); +``` diff --git a/website/src/docs/hotchocolate/v15/api-reference/errors.md b/website/src/docs/hotchocolate/v15/api-reference/errors.md new file mode 100644 index 00000000000..c5d11a5a859 --- /dev/null +++ b/website/src/docs/hotchocolate/v15/api-reference/errors.md @@ -0,0 +1,71 @@ +--- +title: Errors +--- + +GraphQL errors in Hot Chocolate are passed to the operation result by returning an instance of `IError` or an enumerable of `IError` in a field resolver. + +Moreover, you can throw a `GraphQLException` that will be be caught by the execution engine and translated to a field error. + +One further way to raise an error are non-terminating field errors. This can be raised by using `IResolverContext.ReportError`. With this you can provide a result and raise an error for your current field. + +> If you do want to log errors head over to our diagnostic source [documentation](/docs/hotchocolate/v15/server/instrumentation) and see how you can hook up your logging framework of choice to it. + +# Error Builder + +Since errors can have a lot of properties, we have introduced a new error builder which provides a nice API without thousands of overloads. + +```csharp +var error = ErrorBuilder + .New() + .SetMessage("This is my error.") + .SetCode("FOO_BAR") + .Build(); +``` + +# Error Filters + +If some other exception is thrown during execution, then the execution engine will create an instance of `IError` with the message **Unexpected Execution Error** and the actual exception assigned to the error. However, the exception details will not be serialized so by default the user will only see the error message **Unexpected Execution Error**. + +If you want to translate exceptions into errors with useful information then you can write an `IErrorFilter`. + +An error filter has to be registered as a service. + +```csharp +builder.Services.AddErrorFilter(); +``` + +It is also possible to just register the error filter as a delegate like the following. + +```csharp +builder.Services.AddErrorFilter(error => +{ + if (error.Exception is NullReferenceException) + { + return error.WithCode("NullRef"); + } + + return error; +}); +``` + +Since errors are immutable we have added some helper functions like `WithMessage`, `WithCode` and so on that create a new error with the desired properties. Moreover, you can create an error builder from an error and modify multiple properties and then rebuild the error object. + +```csharp +return ErrorBuilder + .FromError(error) + .SetMessage("This is my error.") + .SetCode("FOO_BAR") + .Build(); +``` + +# Exception Details + +In order to automatically add exception details to your GraphQL errors, you can enable the `IncludeExceptionDetails` option. By default this will be enabled when the debugger is attached. + +```csharp +builder + .AddGraphQL() + .ModifyRequestOptions( + o => o.IncludeExceptionDetails = + builder.Environment.IsDevelopment()); +``` diff --git a/website/src/docs/hotchocolate/v15/api-reference/executable.md b/website/src/docs/hotchocolate/v15/api-reference/executable.md new file mode 100644 index 00000000000..a339851f0cd --- /dev/null +++ b/website/src/docs/hotchocolate/v15/api-reference/executable.md @@ -0,0 +1,127 @@ +--- +title: Executable +--- + +The `IExecutable` and `IExecutable` interfaces are intended to be used by data providers. +These interfaces can abstract any kind of data source. +The data or domain layer can wrap data in an executable and pass it to the GraphQL layer. +A GraphQL resolver that returns an `IExecutable` is recognized as a list. + +```csharp +public class User +{ + public string Name { get; } +} + +public interface IUserRepository +{ + public IExecutable FindAll(); +} + +public class Query +{ + public IExecutable GetUsers(IUserRepository repo) => + repo.FindAll(); +} +``` + +```sdl +type Query { + users: [User!]! +} +``` + +This abstraction can be used to completely decouple the GraphQL layer form the database-specific knowledge. + +Filtering, sorting, projections et al, can pick up the executable and apply logic to it. There is still +a database-specific provider needed for these features, but it is opaque to the GraphQL layer. + +The `IExecutable` is known to the execution engine. The engine calls `ToListAsync`, `FirstOrDefault` or +`SingleOrDefault` on the executable. The executable shall execute it in the most efficient way for the +database. + +# API + +## Source + +```csharp + object Source { get; } +``` + +The source property stores the current state of the executable + +In the EntityFramework executable this property holds the `IQueryable`. In the `MongoExecutable` it is the +`DbSet` or the `IAggregateFluent`. `Source` is deliberately read-only. If you have a custom implementation +of `IExecutable` and you want to set the `Source`, you should create a method that returns a new executable +with the new source + +## ToListAsync + +```csharp + ValueTask ToListAsync(CancellationToken cancellationToken); +``` + +Should return a list of ``. + +## FirstOrDefault + +```csharp + ValueTask FirstOrDefault(CancellationToken cancellationToken); +``` + +Should return the first element of a sequence, or a default value if the sequence contains no elements. + +## SingleOrDefault + +```csharp + ValueTask SingleOrDefault(CancellationToken cancellationToken); +``` + +Should return the only element of a default value if no such element exists. This method +should throw an exception if more than one element satisfies the condition. + +## Print + +```csharp +string Print(); +``` + +Prints the executable in its current state + +# Example + +```csharp +public class EntityFrameworkExecutable : QueryableExecutable +{ + public IQueryable Source { get; } + + object IExecutable.Source => Source; + + public EntityFrameworkExecutable(IQueryable queryable) : base(queryable) + { + } + + /// + /// Returns a new enumerable executable with the provided source + /// + /// The source that should be set + /// The new instance of an enumerable executable + public QueryableExecutable WithSource(IQueryable source) + { + return new QueryableExecutable(source); + } + + public override async ValueTask ToListAsync(CancellationToken cancellationToken) => + await Source.ToListAsync(cancellationToken).ConfigureAwait(false); + + public override async ValueTask FirstOrDefaultAsync( + CancellationToken cancellationToken) => + await Source.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + + public override async ValueTask SingleOrDefaultAsync( + CancellationToken cancellationToken) => + await Source.SingleOrDefaultAsync(cancellationToken).ConfigureAwait(false); + + public override string Print() => Source.ToQueryString(); +} +``` diff --git a/website/src/docs/hotchocolate/v15/api-reference/extending-filtering.md b/website/src/docs/hotchocolate/v15/api-reference/extending-filtering.md new file mode 100644 index 00000000000..f2ebde3ae27 --- /dev/null +++ b/website/src/docs/hotchocolate/v15/api-reference/extending-filtering.md @@ -0,0 +1,424 @@ +--- +title: Extending Filtering +--- + +> **Work in progress**: This documentation is not yet complete. + +The `HotChocolate.Data` package works with all databases that support `IQueryable`. Included in the +default settings, are all filter operations that work over `IQueryable` on all databases. +Sometimes this is not enough. Some databases might not support `IQueryable`. Some other databases may have +technology-specific operations (e.g. SQL Like). Filtering was designed with extensibility in mind. + +Filtering can be broken down into two basic parts. Schema building and execution. In schema building, +the input types are created. In execution, the data passed by the user is analyzed and translated to a +database query. Both parts can be configured over a convention. + +In theory, you are free to design the structure of filters as it suits you best. +Usually, it makes sense to divide the structure into two parts. The _field_ and the _operation_. + +The query below returns all movies where the franchise is equal to "Star Wars". The _field_ `franchise` where the filter +is applied to and the _operation_ equals (`eq`) that should operate on this field. + +```graphql +{ + movies(where: { franchise: { eq: "Star Wars" } }) { + name + } +} +``` + +Fields can also form paths. In the query below there are two _fields_ `genre` and `totalMovieCount` and one operation equals +`eq` + +```graphql +{ + movies(where: { genre: { totalMovieCount: { eq: 100 } } }) { + name + } +} +``` + +The two queries above show the difference between _fields_ and _operations_ well. A field is always context-specific. +Even when two fields have the same name, like the description of a movie and the description of a genre, they have different meanings. +One field refers to the description of a movie and the other description refers to the description of a genre. +Same name, different meanings. An operation on the other hand, has always the same meaning. +The equals operation (`eq`) do always mean that the value of the selected field, should +be equals to the value that was provided in the query. +Operations can be applied in different contexts, but the operation itself, stays the same. +The name of the operation should be consistent. There should only be one operation that checks for equality. +This operation should always have the same name. + +With this in mind, we can have a deeper dive into filtering. Buckle up, this might get exciting. + +# How everything fits together + +At the core of the configuration API of filtering there sits a convention. The convention holds the whole +configuration that filtering needs to create filter types and to translate them to the database. +During schema creation, the schema builder asks the convention how the schema should look like. +The convention defines the names and descriptions of types and fields and also what the type should be used for properties. +The convention also defines what provider should be used to translate a GraphQL query to a database query. +The provider is the only thing that is used after the schema is built. +Every field or operation in a filter type has a handler annotated. +During schema initialization, these handlers are bound, to the GraphQL fields. The provider can specify which handler should be bound to which field. +During execution, the provider visits the incoming value node and executes the handler on the fields. +This loose coupling allows defining the provider independently of the convention. + +# Filter Convention + +A filter convention is a dotnet class that has to implement the interface `IFilterConvention`. +Instead of writing a convention completely new, it is recommended to extend the base convention `FilterConvention` +This convention is also configurable with a fluent interface, so in most cases you can probably just use the descriptor API. + +## Descriptor + +Most of the capabilities of the descriptor are already documented under `Fetching Data -> Filtering`. +If you have not done this already, it is now the right time to head over to [Filtering](/docs/hotchocolate/v15/fetching-data/filtering) and read the parts about the `FilterConventions` + +There are two things on this descriptor that are not documented in `Fetching Data`: + +### Operation + +```csharp + IFilterOperationConventionDescriptor Operation(int operationId); +``` + +Operations are configured globally. Each operation has a unique identifier. You can find the build-in identifiers in `DefaultFilterOperations`. +This identifier is used in the `FilterInputType`'s to bind operations on a type. Filter operations can also be configured with a fluent interface. +You can specify the name and the description of the operation. This configuration is applied to all operation fields a `FilterInputType` defines. + +```csharp +conventionDescriptor + .Operation(DefaultFilterOperations.Equals) + .Name("equals") + .Description("Compares the value of the input to the value of the field"); +``` + +With this configuration, all equals operations are now no longer names `eq` but `equals` and have a description. + +If you want to create your own operations, you have to choose an identifier. +To make sure to not collide with the framework, choose a number that is higher than 1024. +If you are a framework developer and want to create an extension for HotChocolate, talk to us. +We can assign you a range of operations so you do not collide with the operations defined by users. + +You will need this identifier later, so it probably makes sense to store it somewhere on a class + +```csharp +public static class CustomOperations +{ + public const int Like = 1025; +} + +public static class CustomerFilterConventionExtensions +{ + public static IFilterConventionDescriptor AddInvariantComparison( + this IFilterConventionDescriptor conventionDescriptor) => + conventionDescriptor + .Operation(CustomOperations.Like) + .Name("like"); +} +``` + +To apply this configuration to operations types, you can use the Configure method + +```csharp + conventionDescriptor.Configure( + x => x.Operation(CustomOperations.Like)) +``` + +### Provider + +```csharp + IFilterConventionDescriptor Provider() + where TProvider : class, IFilterProvider; + IFilterConventionDescriptor Provider(TProvider provider) + where TProvider : class, IFilterProvider; + IFilterConventionDescriptor Provider(Type provider); +``` + +On the convention, you can also specify what provider should be used. For now you need just to know +that you can configure the provider here. We will have a closer look at the provider later. + +```csharp +conventionDescriptor.Provider(); +``` + +## Custom Conventions + +Most of the time the descriptor API should satisfy your needs. It is recommended to build extensions +based on the descriptor API, rather than creating a custom convention. +However, if you want to have full control over naming and type creation, you can also override the methods +you need on the `FilterConvention`. + +You can also override the configure method to have a (probably) familiar API experience. + +```csharp +public class CustomConvention : FilterConvention +{ + protected override void Configure(IFilterConventionDescriptor descriptor) + { + descriptor.AddDefaults(); + } + + public override NameString GetTypeName(Type runtimeType) => + base.GetTypeName(runtimeType) + "Suffix"; +} +``` + +# Providers + +Like the convention, a provider can be configured over a fluent interface. +Every filter field or operation has a specific handler defined. The handler translates the operation to the database. +These handlers are stored on the provider. After the schema is initialized, an interceptor visits the filter types and requests a handler from the provider. +The handler is annotated directly on the field. +The provider translates an incoming query into a database query by traversing an input object and executing the handlers on the fields. + +The output of a translation is always some kind of _filter definition_. In case, of `IQueryable` this is an expression. +In case, of MongoDB this is a `FilterDefinition`. Provider, visitor context and handler, operate on and produce this _filter definition_. + +To inspect and analyze the input object, the provider uses a visitor. + +What a visitor is and how you can write you own visitor is explained [here](/docs/hotchocolate/v15/api-reference/visitors) + +Visitors are a powerful yet complex concept, we tried our best to abstract it away. +For most cases, you will not need to create a custom visitor. + +## Provider Descriptor + +The descriptor of a provider is simple. It only has one method: + +```csharp + IFilterProviderDescriptor AddFieldHandler() + where TFieldHandler : IFilterFieldHandler; +``` + +With this method you can register field handlers on the provider. + +## Field Handler + +Every field or operation is annotated with an instance of a `FilterFieldHandler`. When the provider is asked for a handler for a field, it iterates sequentially through the list of existing field handlers and calls the `CanHandle` method. +The first field handler that can handle the field, is annotated on the field. +As the visitor traverses the input object, it calls `TryHandleEnter` as it enters the input field and `TryHandleLeave` as it leaves it. + +> A field handler supports constructor injection and is a singleton. Do not store data on the field handler. use the `context` of the visitor for state management. + +### CanHandle + +```csharp + bool CanHandle( + ITypeCompletionContext context, + IFilterInputTypeDefinition typeDefinition, + IFilterFieldDefinition fieldDefinition); +``` + +Tests if this field handler can handle a field. If it can handle the field it will be attached to it. + +### TryHandleEnter + +```csharp +bool TryHandleEnter( + TContext context, + IFilterField field, + ObjectFieldNode node, + [NotNullWhen(true)] out ISyntaxVisitorAction? action); +``` + +This method is called when the visitor encounters a field. + +- `context` is the context of the visitor +- `field` is the instance of the field that is currently visited +- `node` is the field node of the input object. `node.Value` contains the value of the field. +- `action` If `TryHandleEnter` returns true, the action is used for further processing by the visitor. + +### TryHandleLeave + +```csharp +bool TryHandleLeave( + TContext context, + IFilterField field, + ObjectFieldNode node, + [NotNullWhen(true)] out ISyntaxVisitorAction? action); +``` + +This method is called when the visitor leave the field it previously entered. + +- `context` is the context of the visitor +- `field` is the instance of the field that is currently visited +- `node` is the field node of the input object. `node.Value` contains the value of the field. +- `action` If `TryHandleLeave` returns true, the action is used for further processing by the visitor. + +## Filter Operation Handlers + +There is only one kind of field handler. To make it easier to handle operations, there also exists `FilterOperationHandler`, a more specific abstraction. +You can override `TryHandleOperation` to handle operations. + +## The Context + +As the visitor and the field handlers are singletons, a context object is passed along with the traversal of input objects. +Field handlers can push data on this context, to make it available for other handlers further down in the tree. + +The context contains `Types`, `Operations`, `Errors` and `Scopes`. It is very provider-specific what data you need to store in the context. +In the case of the `IQueryable` provider, it also contains `RuntimeTypes` and knows if the source is `InMemory` or a database call. + +With `Scopes` it is possible to add multiple logical layers to a context. In the case of `IQueryable` this is needed, whenever a new closure starts + +```csharp +// /------------------------ SCOPE 1 -----------------------------\ +// /----------- SCOPE 2 -------------\ +users.Where(x => x.Company.Addresses.Any(y => y.Street == "221B Baker Street")) +``` + +A filter statement that produces the expression above would look like this + +```graphql +{ + users( + where: { + company: { addresses: { any: { street: { eq: "221B Baker Street" } } } } + } + ) { + name + } +} +``` + +A little simplified this is what happens during visitation: + +```graphql +{ + users( + # level[0] = [] + # instance[0] = x + # Create SCOPE 1 with parameter x of type User + where: { + # Push property User.Company onto the scope + # instance[1] = x.Company + # level[1] = [] + company: { + # Push property Company.Addresses onto the scope + # instance[2] x.Company.Addresses + # level[2] = [] + addresses: { + # Create SCOPE 2 with parameter y of type Address + # instance[0] = y + # level[0] = [] + any: { + # Push property Address.Street onto the scope + # instance[1] = y.Street + # level[1] = [] + street: { + # Create and push the operation onto the scope + # instance[2] = y.Street + # level[2] = [y.Street == "221B Baker Street"] + eq: "221B Baker Street" + } + # Combine everything of the current level and pop the property street from the instance + # instance[1] = y.Street + # level[1] = [y.Street == "221B Baker Street"] + } + # Combine everything of the current level, create the any operation and exit SCOPE 2 + # instance[2] = x.Company.Addresses + # level[2] = [x.Company.Addresses.Any(y => y.Street == "221B Baker Street")] + } + # Combine everything of the current level and pop the property street from the instance + # instance[1] = x.Company + # level[1] = [x.Company.Addresses.Any(y => y.Street == "221B Baker Street")] + } + # Combine everything of the current level and pop the property street from the instance + # instance[0] = x + # level[0] = [x.Company.Addresses.Any(y => y.Street == "221B Baker Street")] + } + ) { + name + } +} +``` + +# Extending IQueryable + +The default filtering implementation uses `IQueryable` under the hood. You can customize the translation of queries by registering handlers on the `QueryableFilterProvider`. + +The following example creates a `StringOperationHandler` that supports case insensitive filtering: + +```csharp +// The QueryableStringOperationHandler already has an implementation of CanHandle +// It checks if the field is declared in a string operation type and also checks if +// the operation of this field uses the `Operation` specified in the override property further +// below +public class QueryableStringInvariantEqualsHandler : QueryableStringOperationHandler +{ + public QueryableStringInvariantEqualsHandler(InputParser inputParser) : base(inputParser) + { + } + + // For creating a expression tree we need the `MethodInfo` of the `ToLower` method of string + private static readonly MethodInfo _toLower = typeof(string) + .GetMethods() + .Single( + x => x.Name == nameof(string.ToLower) && + x.GetParameters().Length == 0); + + // This is used to match the handler to all `eq` fields + protected override int Operation => DefaultFilterOperations.Equals; + + public override Expression HandleOperation( + QueryableFilterContext context, + IFilterOperationField field, + IValueNode value, + object parsedValue) + { + // We get the instance of the context. This is the expression path to the property + // e.g. ~> y.Street + Expression property = context.GetInstance(); + + // the parsed value is what was specified in the query + // e.g. ~> eq: "221B Baker Street" + if (parsedValue is string str) + { + // Creates and returns the operation + // e.g. ~> y.Street.ToLower() == "221b baker street" + return Expression.Equal( + Expression.Call(property, _toLower), + Expression.Constant(str.ToLower())); + } + + // Something went wrong 😱 + throw new InvalidOperationException(); + } +} +``` + +This operation handler can be registered on the convention: + +```csharp +public class CustomFilteringConvention : FilterConvention +{ + protected override void Configure(IFilterConventionDescriptor descriptor) + { + descriptor.AddDefaults(); + descriptor.Provider( + new QueryableFilterProvider( + x => x + .AddDefaultFieldHandlers() + .AddFieldHandler())); + } +} + +// and then +builder.Services + .AddGraphQLServer() + .AddFiltering(); +``` + +To make this registration easier, Hot Chocolate also supports convention and provider extensions. +Instead of creating a custom `FilterConvention`, you can also do the following: + +```csharp +builder.Services + .AddGraphQLServer() + .AddFiltering() + .AddConvention( + new FilterConventionExtension( + x => x.AddProviderExtension( + new QueryableFilterProviderExtension( + y => y.AddFieldHandler())))); +``` diff --git a/website/src/docs/hotchocolate/v15/api-reference/language.md b/website/src/docs/hotchocolate/v15/api-reference/language.md new file mode 100644 index 00000000000..42359107510 --- /dev/null +++ b/website/src/docs/hotchocolate/v15/api-reference/language.md @@ -0,0 +1,86 @@ +--- +title: "Language" +--- + +# Abstract Syntax Tree (AST) + +Hot Chocolate seems to focus solely around `ObjectType`, `InputType` et al. These types work as an interface to configure the _GraphQL_ schema. This schema is used to parse and validate incoming requests. Under the hood, every `query`, `mutation` or `subscription` request is parsed into a so-called abstract syntax tree. Each node of this tree denotes a part of the incoming _GraphQL_ query. + +```graphql +query Users { + userName + address { + street + nr + } +} +``` + +```mermaid +graph TD; + OperationDefinitionNode --> s1["SelectionSetNode"] + s1["SelectionSetNode"] --> id5["FieldNode (userName)"] + s1["SelectionSetNode"] --> id1["FieldNode (address)"] + id1["FieldNode (address)"] --> s2["SelectionSetNode"] + s2["SelectionSetNode"] --> id3["FieldNode (street)"] + s2["SelectionSetNode"] --> id4["FieldNode (nr)"] + +``` + +--- + +# Syntax Node + +Every node in a syntax tree implements `ISyntaxNode`. + +> 💡 The `ToString` method of a syntax node prints the corresponding _GraphQL_ syntax. + +This interface defines the `NodeKind` of the node. + +**Node Kinds:** + +| Name | Description (Spec Link) | Context | Example | +| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------- | ------------------------------- | +| Name | [All names. e.g. Field, Argument ...](https://spec.graphql.org/June2018/#sec-Names) | Both | foo | +| NamedType | [Denotes a reference to a type](https://spec.graphql.org/June2018/#NamedType) | Both | Foo | +| ListType | [Definition of a list](https://spec.graphql.org/June2018/#ListType) | Both | \[Foo] | +| NonNullType | [Definition of type that cannot be null](https://spec.graphql.org/June2018/#NonNullType) | Both | Foo! | +| Argument | [Representation of an argument. Has a _Name_ and a _Value_](https://spec.graphql.org/June2018/#sec-Language.Arguments) | Both | foo: "bar" | +| Directive | [Denotes a directive](https://spec.graphql.org/June2018/#sec-Language.Directives) | Query | @foo | +| Document | [Describes a complete file or request a _GraphQL_ service operates on.](https://spec.graphql.org/June2018/#sec-Language.Document) | Query (out) | | +| OperationDefinition | [Describes a graphql operation like `query` `mutation` or `subscription`](https://spec.graphql.org/June2018/#sec-Language.Document) | Query (out) | query Foo {} | +| VariableDefinition | [The variables defined by an operation](https://spec.graphql.org/June2018/#VariableDefinitions) | Query (out) | (\$foo: String) | +| Variable | [A variable](https://spec.graphql.org/June2018/#sec-Language.Variables) | Query (out) | \$foo | +| SelectionSet | [specifies a selection of _Field_, _FragmentSpread_ or _InlineFragment_](https://spec.graphql.org/June2018/#sec-Selection-Sets) | Query (out) | {foo bar} | +| Field | [Describes a field as a part of a selection set](https://spec.graphql.org/June2018/#sec-Language.Fields) | Query (out) | foo | +| FragmentSpread | [Denotes a spread of a `FragmentDefinition`](https://spec.graphql.org/June2018/#FragmentSpread) | Query (out) | ...f1 | +| InlineFragment | [Denotes an inline fragment](https://spec.graphql.org/June2018/#sec-Inline-Fragments) | Query (out) | ... on Foo { bar} | +| FragmentDefinition | [Defines the definition of a fragment](https://spec.graphql.org/June2018/#FragmentDefinition) | Query (out) | fragment f1 on Foo {} | +| IntValue | [Denotes a `int` value](https://spec.graphql.org/June2018/#sec-Int-Value) | Query (in) | 1 | +| StringValue | [Denotes a `string` value](https://spec.graphql.org/June2018/#sec-String-Value) | Query (in) | "bar" | +| BooleanValue | [Denotes a `boolean` value](https://spec.graphql.org/June2018/#sec-Boolean-Value) | Query (in) | true | +| NullValue | [Denotes a `null` value](https://spec.graphql.org/June2018/#sec-Null-Value) | Query (in) | null | +| EnumValue | [Denotes a `enum` value](https://spec.graphql.org/June2018/#sec-Enum-Value) | Query (in) | FOO | +| FloatValue | [Denotes a _Float_ value](https://spec.graphql.org/June2018/#sec-Float-Value) | Query (in) | 0.2 | +| ListValue | [Denotes a _List_ value](https://spec.graphql.org/June2018/#sec-List-Value) | Query (in) | \["string"] | +| ObjectValue | [Denotes a _ObjectValue_ value](https://spec.graphql.org/June2018/#sec-Input-Object-Values) | Query (in) | {foo: "bar" } | +| ObjectField | [Denotes a field of am input object type](https://spec.graphql.org/June2018/#ObjectField) | Query (in) | foo: "bar" | +| SchemaDefinition | [Definition of a schema](https://spec.graphql.org/June2018/#sec-Schema) | Schema | schema {} | +| OperationTypeDefinition | [This defines one of the root operations `Query`, `Mutation` or `Subscription` on the schema-definition](https://spec.graphql.org/June2018/#RootOperationTypeDefinition) | Schema | query:FooQuery | +| ScalarTypeDefinition | [Definition of a scalar](https://spec.graphql.org/June2018/#sec-Scalars) | Schema | scalar JSON | +| ObjectTypeDefinition | [Definition of an object type](https://spec.graphql.org/June2018/#sec-Objects) | Schema | type Foo{} | +| FieldDefinition | [Definition of a field](https://spec.graphql.org/June2018/#FieldDefinition) | Schema | bar:String | +| InputValueDefinition | [Definition of a input value of an argument](https://spec.graphql.org/June2018/#sec-Field-Arguments) | Schema | x: Float | +| InterfaceTypeDefinition | [Definition of an interface](https://spec.graphql.org/June2018/#sec-Interfaces) | Schema | interface NamedEntity {} | +| UnionTypeDefinition | [Definition of an union](https://spec.graphql.org/June2018/#sec-Unions) | Schema | union Ex = Foo \| Bar | +| EnumTypeDefinition | [Definition of an enum](https://spec.graphql.org/June2018/#sec-Enums) | Schema | enum Foo {BAR} | +| EnumValueDefinition | [Definition of an enum value](https://spec.graphql.org/June2018/#sec-Enum) | Schema | BAR | +| InputObjectTypeDefinition | [Definition of an input type definition](https://spec.graphql.org/June2018/#sec-Input-Objects) | Schema | input FooInput {} | +| SchemaExtension | [Definition of a schema extension](https://spec.graphql.org/June2018/#sec-Schema-Extension) | Schema | extend schema {} | +| ScalarTypeExtension | [Definition of a scalar extension](https://spec.graphql.org/June2018/#sec-Scalar-Extensions) | Schema | extend scalar Foo @bar | +| ObjectTypeExtension | [Definition of an object type extension](https://spec.graphql.org/June2018/#sec-Object-Extensions) | Schema | extend type Foo { name} | +| InterfaceTypeExtension | [Definition of an interface type extension](https://spec.graphql.org/June2018/#sec-Interface-Extensions) | Schema | extend interface NamedEntity {} | +| UnionTypeExtension | [Definition of an union type extension](https://spec.graphql.org/June2018/#sec-Union-Extensions) | Schema | extend union Ex = Foo{} | +| EnumTypeExtension | [Definition of an enum type extension](https://spec.graphql.org/June2018/#sec-Enum-Extensions) | Schema | extend enum foo{} | +| InputObjectTypeExtension | [Definition of an input types](https://spec.graphql.org/June2018/#sec-Input-Object-Extensions) | Schema | input foo {} | +| DirectiveDefinition | [Definition of a directive](https://spec.graphql.org/June2018/#sec-Type-System.Directives) | Schema | directive @foo on | diff --git a/website/src/docs/hotchocolate/v15/api-reference/options.md b/website/src/docs/hotchocolate/v15/api-reference/options.md new file mode 100644 index 00000000000..b804ce02085 --- /dev/null +++ b/website/src/docs/hotchocolate/v15/api-reference/options.md @@ -0,0 +1,24 @@ +--- +title: Schema Options +--- + +Hot Chocolate distinguishes between schema and execution options. Schema options relate to the type system and execution options to the query engine. + +| Member | Type | Default | Description | +| ---------------------- | -------- | -------------- | --------------------------------------------------------------------------- | +| `QueryTypeName` | `string` | `Query` | The name of the query type. | +| `MutationTypeName` | `string` | `Mutation` | The name of the mutation type. | +| `SubscriptionTypeName` | `string` | `Subscription` | The name of the subscription type. | +| `StrictValidation` | `bool` | `true` | Defines if the schema is allowed to have errors like missing resolvers etc. | + +The schema options allow to alter the overall execution behavior. The options can be set during schema creation. + +```csharp +SchemaBuilder.New() + .ModifyOptions(opt => + { + opt.QueryTypeName = "Foo"; + }) + ... + .Create() +``` diff --git a/website/src/docs/hotchocolate/v15/api-reference/visitors.md b/website/src/docs/hotchocolate/v15/api-reference/visitors.md new file mode 100644 index 00000000000..d9b7292a4b3 --- /dev/null +++ b/website/src/docs/hotchocolate/v15/api-reference/visitors.md @@ -0,0 +1,151 @@ +--- +title: "Visitors" +--- + +Hot Chocolate creates an abstract syntax tree for every incoming request. The execution engine evaluates this syntax tree in many different ways. Validation is a good example. Every incoming request has to be validated. The execution engine has to be sure that the semantic of the requested document is correct. A set of rules is applied to the syntax tree, to find potential semantic flaws. + +Usually, you do not have to access the _AST_ directly. The AST only becomes significant, when you want to change execution behavior based on the structure of the query. For example features like _Filtering_, _Sorting_, or _Selection_, analyze the incoming query and generate expressions based on it. + +Hot Chocolate provides you with different APIs that support you to traverse these trees. The `SyntaxWalker` is a visitor that has built-in all the logic to _walk down a syntax tree_. + +The `SyntaxWalker` is completely stateless. All the state is on a context object that is passed along. The generic argument `TContext` of `SyntaxWalker` denotes the type of the context. + +To start the visitation of a _GraphQL_ syntax tree, you have to pass the node and the context the visitation should start from to the visitors `Visit` method. + +--- + +# Visitation + +To start the visitation of a _GraphQL_ syntax tree, you have to pass the node and the context the visitation should start from to the visitors `Visit` method. On its way down the syntax tree, the visitor _enters_ a node. The visitor then gets the children of the current node and _enters_ its children. Once the visitor reached a leaf node, it starts walking back up the tree and _leaves_ all the nodes. The visitor provides a virtual `Enter` and a virtual `Leave` method for all _GraphQL_ AST nodes. These methods are called from the visitor as it _enters_ or _leaves_ a node. + +The syntax walker provides a few methods in addition to the `Enter` and `Leave` methods. For these two methods, there are also convenience methods that are called right _before_ and _after_ the method call. Namely, `OnBeforeEnter`, `OnAfterEnter`, `OnBeforeLeave`, `OnAfterLeave`. +These methods can modify the current `TContext`. These _before_ and _after_ methods are good places to initialize state that is used in the main _enter_ or _leave_ method. e.g. before entering a `FieldNode`, you may want to peek the latest type from the context and get the instance of the `ObjectField` corresponding to `FieldNode` of this type. You may also want to push this type onto the context to then use it in the `Enter` method. + +> **⚠️ NOTE:** In the following sequence diagram the participants do **NOT** represent any object instances. Furthermore, many steps are hidden in this example. The visualization below should just give you provide you visual insight on the order of the methods being called. + +```graphql +query GetFoos { + foo { + bar + } +} +``` + +```mermaid +sequenceDiagram +autonumber + Root->>Root: OnBeforeEnter `query GetFoos` + Root->>Root: Enter `query GetFoos` + Root->>Root: OnAfterEnter `query GetFoos` + Root->>Foo: VisitChildren + Foo->>Foo: OnBeforeEnter foo + Foo->>Foo: Enter foo + Foo->>Foo: OnAfterEnter foo + Foo->>Bar: VisitChildren + Note right of Bar: ... + Bar-->Foo: - + Foo->>Foo: OnBeforeLeave foo + Foo->>Foo: Leave foo + Foo->>Foo: OnAfterLeave foo + Foo-->Root: - + Root->>Root: OnBeforeLeave `query GetFoos` + Root->>Root: Leave `query GetFoos` + Root->>Root: OnAfterLeave `query GetFoos` +``` + +1. We start walking down the tree and _enter_.
Call the `csharp±OnBeforeEnter(OperationDefinitionNode node, TContext context)` +2. Call the `csharp±Enter(OperationDefinitionNode node, TContext context)` +3. Call the `csharp±OnAfterEnter(OperationDefinitionNode node, TContext context)` +4. Call the `csharp±VisitChildren(OperationDefinitionNode node, TContext context)` +5. Call the `csharp±OnBeforeEnter(ObjectFieldNode node, TContext context)` +6. Call the `csharp±Enter(ObjectFieldNode node, TContext context)` +7. Call the `csharp±OnAfterEnter(ObjectFieldNode node, TContext context)` +8. Call the `csharp±VisitChildren(ObjectFieldNode node, TContext context)` +9. We walk back up the tree and _leave_ +10. Call the `csharp±OnBeforeLeave(ObjectFieldNode node, TContext context)` +11. Call the `csharp±Leave(ObjectFieldNode node, TContext context)` +12. Call the `csharp±OnAfterLeave(ObjectFieldNode node, TContext context)` +13. We walk back up the tree and _leave_. +14. Call the `csharp±OnBeforeLeave(OperationDefinitionNode node, TContext context)` +15. Call the `csharp±Leave(OperationDefinitionNode node, TContext context)` +16. Call the `csharp±OnAfterLeave(OperationDefinitionNode node, TContext context)` + +--- + +# Visitor Actions + +The _Enter_ and _Leave_ methods return visitor actions. These methods control the visitors' next step in the visitation. Visitor actions can be used to _skip_ further visitation and step back up, or to _continue_ and walk the current branch of the tree further down. + +## Continue + +If `Continue` is returned from the `Enter` or `Leave` method visitation on the current branch continues. + +In the following example `Continue` is returned from the onEnter method. The visitor calls `VisitChildren` and continues by _entering_ the selection set. + +```graphql {4} +query { + foo { + bar + baz @onEnter(return: CONTINUE) { + quux + } + qux + } +} +``` + +## Skip + +If `Skip` is returned from the `Enter` or `Leave` method, further visitation on this node stops. + +In the following example `Skip` is returned from the onEnter method. The visitor skips the field _baz_. It continues visitation by _entering_ the field _qux_. + +```graphql {4} +query { + foo { + bar + baz @onEnter(return: SKIP) { + quux + } + qux + } +} +``` + +## SkipAndLeave + +If `SkipAndLeave` is returned from the `Enter` method, further visitation on this node stops. Instead of directly calling the next `Enter` method. The visitor calls the `Leave` method of the current node first. + +In the following example `SkipAndLeave` is returned from the onEnter method. The visitor skips the field _baz_. Before it continues visitation with the field _qux_ it _leaves_ the field _baz_ by calling `Leave` + +```graphql {4} +query { + foo { + bar + baz @onEnter(return: SKIPANDLEAVE) { + quux + } + qux + } +} +``` + +## Break + +If `Break` is returned from the `Enter` or `Leave` method, further visitation on this branch stops. + +In the following example `Break` is returned from the onEnter method. The visitor immediately starts walking back up. The visitor calls the `Leave` on `foo` instead of visiting the selections set of _baz_ it skips _baz_ and _qux_. + +```graphql {4} +query { + foo { + bar + baz @onEnter(return: BREAK) { + quux + } + qux + } +} +``` + + diff --git a/website/src/docs/hotchocolate/v15/defining-a-schema/arguments.md b/website/src/docs/hotchocolate/v15/defining-a-schema/arguments.md new file mode 100644 index 00000000000..6fc387e7aa6 --- /dev/null +++ b/website/src/docs/hotchocolate/v15/defining-a-schema/arguments.md @@ -0,0 +1,122 @@ +--- +title: "Arguments" +--- + +GraphQL allows us to specify arguments on a field and access their values in the field's resolver. + +```sdl +type Query { + user(id: ID!): User +} +``` + +Clients can specify arguments like the following. + +```graphql +{ + user(id: "123") { + username + } +} +``` + +Often times arguments will be specified using variables. + +```graphql +query ($userId: ID!) { + user(id: $userId) { + username + } +} +``` + +Learn more about arguments [here](https://graphql.org/learn/schema/#arguments) and variables [here](https://graphql.org/learn/queries/#variables). + +# Usage + +Arguments can be defined like the following. + + + + +```csharp +public class Query +{ + public User GetUser(string username) + { + // Omitted code for brevity + } +} +``` + +We can also change the name of the argument used in the schema. + +```csharp +public class Query +{ + public User GetUser([GraphQLName("name")] string username) + { + // Omitted code for brevity + } +} +``` + + + + +```csharp +public class QueryType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Name(OperationTypeNames.Query); + + descriptor + .Field("user") + .Argument("username", a => a.Type>()) + .Resolve(context => + { + var username = context.ArgumentValue("username"); + + // Omitted code for brevity + }); + } +} +``` + +We can also access nullable values through an `Optional`. + +```csharp +var username = context.ArgumentOptional("username"); + +if (username.HasValue) +{ + // use username.Value +} +``` + + + + +```csharp +builder.Services + .AddGraphQLServer() + .AddDocumentFromString(@" + type Query { + user(username: String!): User + } + ") + .AddResolver("Query", "user", (context) => + { + var username = context.ArgumentValue("username"); + + // Omitted code for brevity + }); +``` + + + + +Arguments can be made required by using the non-null type. Learn more about [non-null](/docs/hotchocolate/v15/defining-a-schema/non-null) + +If we need to provide complex objects as arguments, we can use [input object types](/docs/hotchocolate/v15/defining-a-schema/input-object-types). diff --git a/website/src/docs/hotchocolate/v15/defining-a-schema/directives.md b/website/src/docs/hotchocolate/v15/defining-a-schema/directives.md new file mode 100644 index 00000000000..e5eddfb5b2e --- /dev/null +++ b/website/src/docs/hotchocolate/v15/defining-a-schema/directives.md @@ -0,0 +1,412 @@ +--- +title: "Directives" +--- + +Directives provide a way to add metadata for client tools such as code generators and IDEs or alternate a GraphQL server's runtime execution and type validation behavior. + +There are two kinds of directives, executable directives to annotate executable parts of GraphQL documents and type-system directives to annotate SDL types. + +Typically, any GraphQL server implementation should provide the following directives `@skip`, `@include`, and `@deprecated`. `@skip` and `@include`, for example, are executable directives used in GraphQL documents to exclude or include fields, whereas `@deprecated` is a type-system directive used in SDL types to inform client tools that a particular part such as a field is deprecated. + +# Structure + +Directives consist of a name and zero or more arguments. `@skip`, for example, has the name **skip** and a mandatory argument named **if**. Also, `@skip` carries a piece of hidden information only examinable in SDL, namely the location, which specifies where a directive is applicable. Let's take a look at the SDL of the `@skip` directive. + +```sdl +directive @skip(if: Boolean!) on + | FIELD + | FRAGMENT_SPREAD + | INLINE_FRAGMENT +``` + +The `directive` keyword in SDL indicates that we're dealing with a directive type declaration. The `@` sign also indicates that this is a directive but more from a usage perspective. + +The word `skip` represents the directive's name followed by a pair of parentheses that includes a list of arguments, consisting, in our case, of one argument named `if` of type non-nullable boolean (meaning it is required). + +The `on` keyword indicates the location where or at which part a directive is applicable, followed by a list of exact locations separated by pipes `|`. In the case of `@skip`, we can see that we're dealing with an executable directive because this directive is only applicable to fields, fragment-spreads, and inline-fragments. + +# Usage + +Let's say we have a GraphQL document and want to exclude details under certain circumstances; it would probably look something like this. + +```graphql +query me($excludeDetails: Boolean!) { + me { + id + name + ...Details @skip(if: $excludeDetails) + } +} + +fragment Details on User { + mobileNumber + phoneNumber +} +``` + +With `@skip`, we've successfully altered the GraphQL's runtime execution behavior. If `$excludeDetails` is set to `true`, the execution engine will exclude the fields `mobileNumber` and `phoneNumber`; the response would look like this. + +```json +{ + "data": { + "me": { + "id": "VXNlcgox", + "name": "Henry" + } + } +} +``` + +Now that we know how to use directives in GraphQL, let's head over to the next section, which is about one crucial aspect of directives. + +## Order Matters + +**The order of directives is significant**, because the execution is in **sequential order**, which means one after the other. If we have something like the following example, we can see how directives can affect each other. + +```graphql +query me { + me { + name @skip(if: true) @include(if: true) + } +} +``` + +Since we excluded the field `name` in the first place, `@include` does not affect the field `name` anymore. We then just get an empty `me` object in return. + +```json +{ + "data": { + "me": {} + } +} +``` + +> **Note:** We will have a deep dive on directives' order under the [Middleware](#order) section. + +Now that we have a basic understanding of what directives are, how they work, and what we can do with them, let's create a custom directive. + +# Custom Directives + +To create a directive, we need to create a new class that inherits from `DirectiveType` and also to override the `Configure` method. + +```csharp +public class MyDirectiveType : DirectiveType +{ + protected override void Configure(IDirectiveTypeDescriptor descriptor) + { + descriptor.Name("my"); + descriptor.Location(DirectiveLocation.Field); + } +} +``` + +[Learn more about Locations](#locations) + +We also have to register the directive explicitly. + +```csharp +builder.Services + .AddGraphQLServer() + .AddDirectiveType(); +``` + +Let's recap! We have registered a new directive named `my` without any arguments and limited the usage to fields only. A GraphQL query request with our new directive could look like this. + +```graphql +query foo { + bar @my +} +``` + +As of now, our custom directive provides no functionality. We will handle that part in the [Middleware](#middleware) section. But before that, let's talk about repeatable directives and arguments. + +## Repeatable + +By default, directives are not repeatable, which means directives are unique and can only be applied once at a specific location. For example, if we use the `my` directive twice at the field `bar`, we will encounter a validation error. So the following GraphQL query request results in an error if the directive is not repeatable. + +```graphql +query foo { + bar @my @my +} +``` + +We can enable repeatability like the following. + +```csharp +public class MyDirectiveType : DirectiveType +{ + protected override void Configure(IDirectiveTypeDescriptor descriptor) + { + descriptor.Name("my"); + descriptor.Location(DirectiveLocation.Field); + descriptor.Repeatable(); + } +} +``` + +This configuration will translate into the following SDL. + +```sdl +directive @my repeatable on FIELD +``` + +## Arguments + +A directive can provide additional information through arguments. +They might also come in handy, in combination with repeatable directives, for reusability purposes. + +We can add an argument like the following. + +```csharp +public class MyDirective +{ + public string Name { get; set; } +} + +public class MyDirectiveType : DirectiveType +{ + protected override void Configure( + IDirectiveTypeDescriptor descriptor) + { + descriptor.Name("my"); + descriptor.Location(DirectiveLocation.FieldDefinition); + + // The 'Name' property is included as an argument implicitly + + // descriptor + // .Argument(f => f.ChangeMe) + // .Type>() + // .Name("differentName"); + // descriptor.Ignore(f => f.IgnoreMe); + } +} +``` + +If we prefer to not use a backing POCO (``) we an also use the `Argument()` method on the `descriptor`. + +```csharp +public class MyDirectiveType : DirectiveType +{ + protected override void Configure(IDirectiveTypeDescriptor descriptor) + { + descriptor.Name("my"); + descriptor.Location(DirectiveLocation.Field); + + descriptor + .Argument("name") + .Type>(); + } +} +``` + +This configuration will translate into the following SDL. + +```sdl +directive @my(name: String!) on FIELD +``` + +## Usage within Types + +We could associate the `MyDirectiveType` with an object type like the following. + +```csharp +public class FooType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Name("Foo"); + descriptor.Directive("my", new ArgumentNode("name", "bar")); + } +} +``` + +> Note: For this to work the `MyDirectiveType` directive needs to have the appropriate location within the schema. In this example it would be `DirectiveLocation.Object`. + +Referencing directives using their name is not type-safe and could lead to runtime errors, which are avoidable by using our generic variant of the directive type. + +Once we have defined our directive using `DirectiveType`, we can pass an instance of the backing POCO (``) instead of the name of the directive and an `ArgumentNode`. + +```csharp +public class FooType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Name("Foo"); + descriptor.Directive(new MyDirective { Name = "bar" }); + } +} +``` + +Since the directive instance that we have added to our type is now a strong .NET type, we don't have to fear changes to the directive structure or name anymore. + +## Locations + +A directive can define one or multiple locations, where it can be applied. Multiple locations are separated by a pipe `|`. + +```csharp +descriptor.Location(DirectiveLocation.Field | DirectiveLocation.Object); +``` + +Generally we distinguish between two types of locations: Type system and executable locations. + +### Type System Locations + +Type system locations specify where we can place a specific directive in the schema. The arguments of directives specified in these locations are fixed. We can query such directives through introspection. + +The following schema shows where type system directives can be applied. + +```sdl +directive @schema on SCHEMA +directive @object on OBJECT +directive @argumentDefinition on ARGUMENT_DEFINITION +directive @fieldDefinition on FIELD_DEFINITION +directive @inputObject on INPUT_OBJECT +directive @inputFieldDefinition on INPUT_FIELD_DEFINITION +directive @interface on INTERFACE +directive @enum on ENUM +directive @enumValue on ENUM_VALUE +directive @union on UNION +directive @scalar on SCALAR +schema @schema { + query: Query +} +type Query @object { + search(by: SearchInput! @argumentDefinition): SearchResult @fieldDefinition +} +input SearchInput @inputObject { + searchTerm: String @inputFieldDefinition +} +interface HasDescription @interface { + description: String +} +type Product implements HasDescription { + added: DateTime + description: String +} +enum UserKind @enum { + Administrator @enumValue + Moderator +} +type User { + name: String + userKind: UserKind +} +union SearchResult @union = Product | User +scalar DateTime @scalar +``` + +### Executable Locations + +Executable locations specify where a client can place a specific directive, when executing an operation. + +Our server defines the following directives. + +```sdl +directive @query on QUERY +directive @field on FIELD +directive @fragmentSpread on FRAGMENT_SPREAD +directive @inlineFragment on INLINE_FRAGMENT +directive @fragmentDefinition on FRAGMENT_DEFINITION +directive @mutation on MUTATION +directive @subscription on SUBSCRIPTION +``` + +The following request document shows where we, as a client, can apply these directives. + +```graphql +query getUsers @query { + search(by: { searchTerm: "Foo" }) @field { + ...DescriptionFragment @fragmentSpread + ... on User @inlineFragment { + userKind + } + } +} + +fragment DescriptionFragment on HasDescription @fragmentDefinition { + description +} + +mutation createNewUser @mutation { + createUser(input: { name: "Ada Lovelace" }) { + user { + name + } + } +} + +subscription subscribeToUser @subscription { + onUserChanged(id: 1) { + user { + name + } + } +} +``` + +## Middleware + +What makes directives in Hot Chocolate very useful is the ability to associate a middleware with it. A middleware can alternate the result, or even produce the result, of a field. A directive middleware is only added to a field middleware pipeline when the directive was annotated to the object definition, the field definition or the field. + +Moreover, if the directive is repeatable the middleware will be added multiple times to the middleware allowing to build a real pipeline with it. + +In order to add a middleware to a directive we could declare it with the descriptor as a delegate. + +```csharp +public class MyDirectiveType : DirectiveType +{ + protected override void Configure( + IDirectiveTypeDescriptor descriptor) + { + descriptor.Name("my"); + descriptor.Location(DirectiveLocation.Object); + + descriptor.Use((next, directive) => context => + { + context.Result = "Bar"; + return next.Invoke(context); + }); + } +} +``` + +Directives with middleware or executable directives can be put on object types and on their field definitions or on the field selection in a query. Executable directives on an object type will replace the field resolver of every field of the annotated object type. + +### Order + +In GraphQL the order of directives is significant and with our middleware we use this order to create a resolver pipeline through which the result flows. + +The resolver pipeline consists of a sequence of directive delegates, called one after the other. + +Each delegate can perform operations before and after the next delegate. A delegate can also decide to not pass a resolver request to the next delegate, which is called short-circuiting the resolver pipeline. +Short-circuiting is often desirable because it avoids unnecessary work. + +The order of the middleware pipeline is defined by the order of the directives. Since executable directives will flow from the object type to its field definitions, the directives of the type would be called first in the order that they were annotated. + +```sdl +type Query { + foo: Bar +} + +type Bar @a @b { + baz: String @c @d +} +``` + +So, the directives in the above example would be called in the following order `a, b, c, d`. + +If there were more directives in the query, they would be appended to the directives from the type. + +```graphql +{ + foo { + baz @e @f + } +} +``` + +So, now the order would be like the following: `a, b, c, d, e, f`. + +Every middleware can execute the original resolver function by calling `ResolveAsync()` on the `IDirectiveContext`. + + diff --git a/website/src/docs/hotchocolate/v15/defining-a-schema/documentation.md b/website/src/docs/hotchocolate/v15/defining-a-schema/documentation.md new file mode 100644 index 00000000000..cee76adf96b --- /dev/null +++ b/website/src/docs/hotchocolate/v15/defining-a-schema/documentation.md @@ -0,0 +1,245 @@ +--- +title: Documentation +--- + +Documentation allows us to enrich our schema with additional information that is useful for a consumer of our API. + +In GraphQL we can do this by providing descriptions to our types, fields, etc. + +```sdl +type Query { + "A query field" + user("An argument" username: String): User +} + +"An object type" +type User { + "A field" + username: String +} + +"An enum" +enum UserRole { + "An enum value" + ADMINISTRATOR +} +``` + +# Usage + +We can define descriptions like the following. + + + + +```csharp +[GraphQLDescription("An object type")] +public class User +{ + [GraphQLDescription("A field")] + public string Username { get; set; } +} + +[GraphQLDescription("An enum")] +public enum UserRole +{ + [GraphQLDescription("An enum value")] + Administrator +} + +public class Query +{ + [GraphQLDescription("A query field")] + public User GetUser( + [GraphQLDescription("An argument")] string username) + { + // Omitted code for brevity + } +} +``` + +If the description provided to the `GraphQLDescriptionAttribute` is `null` or made up of only white space, XML documentation comments are used as a fallback. + +Learn more about XML documentation below. + + + + +```csharp +public class UserType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Name("User"); + descriptor.Description("An object type"); + + descriptor + .Field(f => f.Username) + .Description("A field"); + } +} + +public class UserRoleType : EnumType +{ + protected override void Configure(IEnumTypeDescriptor descriptor) + { + descriptor.Name("UserRole"); + descriptor.Description("An enum"); + + descriptor + .Value(UserRole.Administrator) + .Description("An enum value"); + } +} + +public class QueryType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Name(OperationTypeNames.Query); + + descriptor + .Field("user") + .Description("A query field") + .Argument("username", a => a.Type() + .Description("An argument")) + .Resolve(context => + { + // Omitted code for brevity + }); + } +} +``` + +The `Description()` methods take precedence over all other forms of documentation. This is true, even if the provided value is `null` or only white space. + + + + +```csharp +builder.Services + .AddGraphQLServer() + .AddDocumentFromString(@" + type Query { + """""" + A query field + """""" + user(""An argument"" username: String): User + } + + """""" + An object type + """""" + type User { + ""A field"" + username: String + } + + """""" + An enum + """""" + enum UserRole { + ""An enum value"" + ADMINISTRATOR + } + ") + // Omitted code for brevity +``` + + + + +# XML Documentation + +Hot Chocolate provides the ability to automatically generate API documentation from our existing [XML documentation](https://docs.microsoft.com/dotnet/csharp/codedoc). + +The following will produce the same schema descriptions we declared above. + +```csharp +/// +/// An object type +/// +public class User +{ + /// + /// A field + /// + public string Username { get; set; } +} + +/// +/// An enum +/// +public enum UserRole +{ + /// + /// An enum value + /// + Administrator +} + +public class Query +{ + /// + /// A query field + /// + /// An argument + public User GetUser(string username) + { + // Omitted code for brevity + } +} +``` + +To make the XML documentation available to Hot Chocolate, we have to enable `GenerateDocumentationFile` in our `.csproj` file. + +```xml + + true + $(NoWarn);1591 + +``` + +> Note: The `` element is optional. It prevents the compiler from emitting warnings for missing documentation strings. + +If we do not want to include XML documentation in our schema, we can set the `UseXmlDocumentation` property on the schema's `ISchemaOptions`. + +```csharp +builder.Services + .AddGraphQLServer() + .ModifyOptions(opt => opt.UseXmlDocumentation = false); +``` + +## With a custom naming convention + +If you want to use a custom naming convention and XML documentation, ensure you give the convention an instance of the `XmlDocumentationProvider` as demonstrated below; otherwise the comments won't appear in your schema. + +```csharp +public class CustomNamingConventions : DefaultNamingConventions +{ + // Before + public CustomNamingConventions() + : base() { } + + // After + public CustomNamingConventions(IDocumentationProvider documentationProvider) + : base(documentationProvider) { } +} + +// Program +// Before +.AddConvention(sp => new CustomNamingConventions()); + +// After +IReadOnlySchemaOptions capturedSchemaOptions; + +builder.Services + .AddGraphQLServer() + .ModifyOptions(opt => capturedSchemaOptions = opt) + .AddConvention(sp => new CustomNamingConventions( + new XmlDocumentationProvider( + new XmlDocumentationFileResolver( + capturedSchemaOptions.ResolveXmlDocumentationFileName), + sp.GetApplicationService>() + ?? new NoOpStringBuilderPool()))); +``` diff --git a/website/src/docs/hotchocolate/v15/defining-a-schema/dynamic-schemas.md b/website/src/docs/hotchocolate/v15/defining-a-schema/dynamic-schemas.md new file mode 100644 index 00000000000..925eb8bf799 --- /dev/null +++ b/website/src/docs/hotchocolate/v15/defining-a-schema/dynamic-schemas.md @@ -0,0 +1,359 @@ +--- +title: Dynamic Schemas +--- + +In the world of SaaS, one size rarely fits all. With the ever-changing requirements and the need for high flexibility, schemas in a web application often need to be dynamic. In the context of GraphQL, a dynamic schema allows you to adapt the data structure exposed by your API according to varying conditions, be it different tenant, changing data sources, or configuration. + +For instance, consider a Content Management System (CMS) where each tenant might require custom fields that are specific to their use case. Having a static GraphQL schema in such a scenario would mean that you need to anticipate all possible custom fields beforehand, which is not practical. A dynamic schema, on the other hand, allows you to add, remove, or modify the types and fields in your schema at runtime based on the specific needs of each tenant. Each tenant can have a different schema, and you can adapt the schema to the tenant's needs without having to redeploy your application. + +While creating dynamic schemas in GraphQL offers substantial flexibility, it also comes with its own set of complexities. This is where the `ITypeModule` interface in Hot Chocolate comes into play. + +# What is `ITypeModule`? + +`ITypeModule` is an interface introduced in Hot Chocolate that allows you to build a component that dynamically provides types to the schema building process. + +The `ITypeModule` interface consists of an event `TypesChanged` and a method `CreateTypesAsync`. Here is a brief overview of each: + +- `TypesChanged`: This event signals when types have changed and the current schema version needs to be phased out. + +- `CreateTypesAsync`: This method is called by the schema building process to create types for a new schema instance. It takes a descriptor context, which provides access to schema building services and conventions, and a cancellation token. + +When the underlying structure for a type module changes, for example, due to alterations in a database schema or updates in a JSON file defining types, the `TypesChanged` event can be triggered. This event tells Hot Chocolate to phase out the old schema and introduce a new one with the updated types from the module. + +In essence, `ITypeModule` takes care of the complexities associated with providing a dynamic schema with hot-reload functionality, allowing developers to focus on the core logic of their applications. + +In the following sections, we'll look at a couple of examples that demonstrate how to use `ITypeModule` to create dynamic schemas in different scenarios. + +# Example: Creating Types from a JSON File + +In this example, we'll explore how to create a dynamic schema from a JSON file. This scenario might be common if your application allows users to define custom types and fields through a UI, and these definitions are stored as JSON. + +Let's consider the following `ITypeModule` implementation: + +```csharp +public class JsonTypeModule : ITypeModule +{ + private readonly string _file; + + public JsonTypeModule(string file) + { + _file = file; + } + + public event EventHandler? TypesChanged; + + public async ValueTask> CreateTypesAsync( + IDescriptorContext context, + CancellationToken cancellationToken) + { + var types = new List(); + + await using var file = File.OpenRead(_file); + using var json = await JsonDocument.ParseAsync(file, cancellationToken: cancellationToken); + + foreach (var type in json.RootElement.EnumerateArray()) + { + var typeDefinition = new ObjectTypeDefinition(type.GetProperty("name").GetString()!); + + foreach (var field in type.GetProperty("fields").EnumerateArray()) + { + typeDefinition.Fields.Add( + new ObjectFieldDefinition( + field.GetString()!, + type: TypeReference.Parse("String!"), + pureResolver: ctx => "foo")); + } + + types.Add( + type.GetProperty("extension").GetBoolean() + ? ObjectTypeExtension.CreateUnsafe(typeDefinition) + : ObjectType.CreateUnsafe(typeDefinition)); + } + + return types; + } +} +``` + +In this implementation, `CreateTypesAsync` reads a JSON file, parses it, and creates types based on the content of the JSON. If any of these types are extensions, they are created as such. If the types or their structure change, you could fire the `TypesChanged` event to signal that a new schema needs to be generated. + +# Unsafe Type Creation + +When working with dynamic schemas and the `ITypeModule` interface, one of the practices you'll encounter is the use of the `CreateUnsafe` method to create types. +The unsafe way to create types, as the name implies, bypasses some of the standard validation logic. This method is useful for advanced scenarios where you need more flexibility, such as when dynamically creating types based on runtime data. + +The `CreateUnsafe` method allows you to create types directly from a `TypeDefinition`. + +```csharp +var typeDefinition = new ObjectTypeDefinition("DynamicType"); +// ... populate typeDefinition ... + +var dynamicType = ObjectType.CreateUnsafe(typeDefinition); +``` + +Using `CreateUnsafe` method for type creation can be a complex task as it involves operating directly on the type definition. +This allows for a lot of flexibility, but it also requires a deeper understanding of the Hot Chocolate type system. + +Here are some examples of how you might use the `CreateUnsafe` method to create various types. + +> This is by no means an exhaustive list, but it should give you an idea of how to use this feature. + +## Creating an Object Type + +Let's say we want to create a new object type representing a `Product` in an e-commerce system. +We would start by defining the `ObjectTypeDefinition`: + +```csharp +var objectTypeDefinition = new ObjectTypeDefinition("Product") +{ + Description = "Represents a product in the e-commerce system", + RuntimeType = typeof(Dictionary) +}; +``` + +Next, we might want to add fields to this object type. For instance, a `Product` might have an `ID`, `Name`, and `Price`: + +```csharp +var idFieldDefinition = new ObjectFieldDefinition( + "id", + "Unique identifier for the product", + TypeReference.Parse("ID!"), + pureResolver: context => context.Parent>()["id"]); + +var nameFieldDefinition = new ObjectFieldDefinition( + "name", + "Name of the product", + TypeReference.Parse("String!"), + pureResolver: context => context.Parent>()["name"]); + +var priceFieldDefinition = new ObjectFieldDefinition( + "price", + "Price of the product", + TypeReference.Parse("Float!"), + pureResolver: context => context.Parent>()["price"]); + +objectTypeDefinition.Fields.Add(idFieldDefinition); +objectTypeDefinition.Fields.Add(nameFieldDefinition); +objectTypeDefinition.Fields.Add(priceFieldDefinition); +``` + +Here, each resolver retrieves the corresponding value from the parent `Dictionary`. + +Next, let's add a field that calculates the price after applying a discount. This field would have an argument specifying the discount percentage: + +```csharp +var discountArgument = new ArgumentDefinition( + "discount", + "Discount percentage to apply", + TypeReference.Parse("Float!")); + +var discountPriceField = new ObjectFieldDefinition( + "discountPrice", + "Price after discount", + TypeReference.Parse("Float!"), + pureResolver: context => + { + var product = context.Parent>(); + var discountPercentage = context.ArgumentValue("discount"); + var originalPrice = (float) product["price"]; + return originalPrice * (1 - discountPercentage / 100); + } +) +{ + Arguments = { discountArgument } +}; + +objectTypeDefinition.Fields.Add(discountPriceField); +``` + +In this case, the `discountPrice` field takes a `discount` argument and uses it to calculate the discounted price. The resolver retrieves the original price from the parent `Dictionary`, applies the discount, and returns the discounted price. + +Finally, we create the `ObjectType` and register it: + +```csharp +var productType = ObjectType.CreateUnsafe(objectTypeDefinition); +builder.Services + .AddGraphQLServer() + .AddQueryType() + ... // other configuration + .AddType(productType); +``` + +Now our `Product` object type has fields `id`, `name`, `price`, and `discountPrice(discount: Float!)`. The `discountPrice` field takes a `discount` argument representing the discount percentage. + +## Resolver types + +A resolver in Hot Chocolate is a delegate that fetches the data for a specific field. There are two types of resolvers: _async Resolvers_ and _pure Resolvers_. + +1. **Async Resolvers**: + + ```csharp + public delegate ValueTask FieldResolverDelegate(IResolverContext context); + ``` + + _Async Resolvers_ are are typically async and have access to a `IResolverContext`. They are usually used for fetching data from services or databases. + +2. **Pure Resolvers**: + + ```csharp + public delegate object? PureFieldDelegate(IResolverContext context); + ``` + + _Pure Resolvers_ is used where no side-effects or async calls are needed. All your properties are turned into pure resolvers by Hot Chocolate. + The execution engine optimizes the execution of these resolvers (through inlining of the value completion) to make it significantly faster. + +The decision to use _async Resolvers_ or _pure Resolvers_ depends on your use case. If you need to perform asynchronous operations,or fetch data from services, you would use _async Resolvers_. If your resolver is simply retrieving data without any side effects, _pure Resolvers_ would be a more performant choice. + +Let's add a non-pure field resolver to our example. For instance, we can add a `reviews` field that fetches reviews for a product from an external service: + +```csharp +var reviewsFieldDefinition = new ObjectFieldDefinition( + "reviews", + "Reviews for the product", + TypeReference.Parse("[Review!]"), + resolver: async context => + { + var productId = context.Parent>()["id"]; + var reviewsService = context.Service(); + return await reviewsService.GetReviewsForProduct(productId); + }); + +objectTypeDefinition.Fields.Add(reviewsFieldDefinition); +``` + +Here, `IReviewsService` could be an interface representing a service that fetches reviews. The `reviewsResolver` uses the `Service` method on the `IMiddlewareContext` to retrieve an instance of this service, then calls a method on this service to get the reviews. . + +This field resolver is a `FieldResolverDelegate` (i.e., a non-pure resolver) because it needs perform an asynchronous operation. + +The resulting schema is: + +```graphql +"Represents a product in the e-commerce system" +type Product { + "Unique identifier for the product" + id: ID! + "Name of the product" + name: String! + "Price of the product" + price: Float! + "Price after discount" + discountPrice("Discount percentage to apply" discount: Float!): Float! + "Reviews for the product" + reviews: String +} +``` + +## Creating an Input Object Type + +Creating an Input Object Type is very similar to creating an Object Type. The major difference lies in the fact that Input Object Types are used in GraphQL mutations or as arguments in queries, whereas Object Types are used in GraphQL queries to define the shape of the returned data. Meaning you don't need to define resolvers for Input Object Types. + +An Input Object Type can be created by defining an `InputObjectTypeDefinition` and using the `InputObjectType.CreateUnsafe` method. + +Let's create an input object type representing a `ProductInput` which can be used to create or update a product: + +```csharp +var inputObjectTypeDefinition = new InputObjectTypeDefinition("ProductInput") +{ + Description = "Represents product input for creating or updating a product", + RuntimeType = typeof(Dictionary) +}; + +var nameFieldDefinition = new InputFieldDefinition( + "name", + "Name of the product", + TypeReference.Parse("String!")); + +var priceFieldDefinition = new InputFieldDefinition( + "price", + "Price of the product", + TypeReference.Parse("Float!")); + +inputObjectTypeDefinition.Fields.Add(nameFieldDefinition); +inputObjectTypeDefinition.Fields.Add(priceFieldDefinition); + +var productInputType = InputObjectType.CreateUnsafe(inputObjectTypeDefinition); +builder.Services + .AddGraphQLServer() + .AddQueryType() + ... // other configuration + .AddType(productInputType); +``` + +As with Object Types, you can use the `CreateUnsafe` method to create complex input types based on runtime data. + +## Combining the Generated Types + +To create a GraphQL mutation, you need an `InputObjectType` to define the input of the mutation and an `ObjectType` to define the output. You can create a mutation by defining a `MutationTypeDefinition` and using the `MutationType.CreateUnsafe` method. + +Let's extend the previous examples to create a `createProduct` mutation using the `ProductInput` and `Product` types: + +```csharp +var createProductMutationFieldDefinition = new ObjectFieldDefinition( + "createProduct", + "Creates a new product", + TypeReference.Parse("Product!"), + resolver: async context => + { + var productInput = context.ArgumentValue>("input"); + var productService = context.Service(); + var newProduct = await productService.CreateProduct(productInput); + return newProduct; + } +) +{ + Arguments = + { + new ArgumentDefinition( + "input", + "Input for creating the product", + TypeReference.Parse("ProductInput!")) + } +}; + +var mutationTypeDefinition = new ObjectTypeDefinition("Mutation") +{ + RuntimeType = typeof(object) +}; + +mutationTypeDefinition.Fields.Add(createProductMutationFieldDefinition); + +var mutationType = ObjectType.CreateUnsafe(mutationTypeDefinition); +builder.Services + .AddGraphQLServer() + .AddQueryType() + .AddMutationType(mutationType) + ... // other configuration + .AddType(productInputType) + .AddType(productType); +``` + +In this example, we first define a mutation field `createProduct` that takes a `ProductInput` argument and returns a `Product`. The resolver for this field uses a hypothetical `IProductService` to create a new product based on the input. + +We then define a `Mutation` type and add the `createProduct` field to it. Finally, we use the `CreateUnsafe` method to create the `Mutation` type and register it along with the `ProductInput` and `Product` types. + +With this setup, you can now use the `createProduct` mutation in your GraphQL API: + +```graphql +mutation CreateProduct($input: ProductInput!) { + createProduct(input: $input) { + id + name + price + } +} +``` + +With the variable: + +```json +{ + "input": { + "name": "New Product", + "price": 99.99 + } +} +``` + +This mutation will create a new product and return its details as a `Product` object. + +This way, you can use the generated `InputObjectType` and `ObjectType` together to create a complete GraphQL mutation. Similarly, you can combine other generated types to create the queries, subscriptions, and other parts of your GraphQL schema. diff --git a/website/src/docs/hotchocolate/v15/defining-a-schema/enums.md b/website/src/docs/hotchocolate/v15/defining-a-schema/enums.md new file mode 100644 index 00000000000..70f6cfd04b4 --- /dev/null +++ b/website/src/docs/hotchocolate/v15/defining-a-schema/enums.md @@ -0,0 +1,369 @@ +--- +title: "Enums" +--- + +An Enum is a special kind of [scalar](/docs/hotchocolate/v15/defining-a-schema/scalars) that is restricted to a particular set of allowed values. It can be used as both an input and an output type. + +```sdl +enum UserRole { + GUEST, + DEFAULT, + ADMINISTRATOR +} + +type Query { + role: UserRole + usersByRole(role: UserRole): [User] +} +``` + +# Usage + +Given is the schema from above. + +When querying a field returning an enum type, the enum value will be serialized as a string. + +**Request** + +```graphql +{ + role +} +``` + +**Response** + +```json +{ + "data": { + "role": "STANDARD" + } +} +``` + +When using an enum value as an argument, it is represented as a literal and **not** a string. + +**Request** + +```graphql +{ + usersByRole(role: ADMINISTRATOR) { + id + } +} +``` + +When used as a type for a variable, it is represented as a string in the variables object, since JSON does not offer support for literals. + +**Request** + +Operation: + +```graphql +query ($role: UserRole) { + usersByRole(role: $role) { + id + } +} +``` + +Variables: + +```json +{ + "role": "ADMINISTRATOR" +} +``` + +# Definition + +We can define enums like the following. + + + + +```csharp +public enum UserRole +{ + Guest, + Standard, + Administrator +} + +public class Query +{ + public User[] GetUsers(UserRole role) + { + // Omitted code for brevity + } +} +``` + + + + +```csharp +public enum UserRole +{ + Guest, + Standard, + Administrator +} + +public class UserRoleType : EnumType +{ +} + +public class QueryType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Name(OperationTypeNames.Query); + + descriptor + .Field("users") + .Argument("role", a => a.Type()) + .Resolve(context => + { + var role = context.ArgumentValue("role"); + + // Omitted code for brevity + }); + } +} +``` + +Since there could be multiple enum types inheriting from `EnumType`, but differing in their name and values, it is not certain which of these types should be used when we return a `UserRole` CLR type from one of our resolvers. + +**Therefore it's important to note that code-first enum types are not automatically inferred. They need to be explicitly specified or registered.** + +We can either [explicitly specify the type on a per-resolver basis](/docs/hotchocolate/v15/defining-a-schema/object-types#explicit-types) or we can register the type once globally: + +```csharp +builder.Services + .AddGraphQLServer() + .AddType(); +``` + +With this configuration each `UserRole` CLR type we return from our resolvers would be assumed to be a `UserRoleType`. + + + + +```csharp +builder.Services + .AddGraphQLServer() + .AddDocumentFromString(@" + type Query { + user(role: UserRole): User + } + + enum UserRole { + GUEST, + DEFAULT, + ADMINISTRATOR + } + ") + .AddResolver("Query", "user", (context) =>- + { + var role = context.ArgumentValue("role"); + + // Omitted code for brevity + }) +``` + + + + +## Non-enum values + +In code-first we can also bind the enum type to any other .NET type, for example a `string`. + +```csharp +public class UserRoleType : EnumType +{ + protected override void Configure(IEnumTypeDescriptor descriptor) + { + // we need to specify a name or otherwise we will get a conflict + // with the built-in StringType + descriptor.Name("UserRole"); + + descriptor + .Value("Default") + .Name("STANDARD"); + } +} + +public class QueryType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Name(OperationTypeNames.Query); + + descriptor + .Field("users") + .Argument("role", a => a.Type()) + .Resolve(context => + { + var role = context.ArgumentValue("role"); + + // Omitted code for brevity + }); + } +} +``` + +# Binding behavior + +In the implementation-first approach all enum values are implicitly included on the schema enum type. The same is true for `T` of `EnumType` when using the code-first approach. + +In the code-first approach we can also enable explicit binding, where we have to opt-in enum values we want to include instead of them being implicitly included. + + + +We can configure our preferred binding behavior globally like the following. + +```csharp +builder.Services + .AddGraphQLServer() + .ModifyOptions(options => + { + options.DefaultBindingBehavior = BindingBehavior.Explicit; + }); +``` + +> Warning: This changes the binding behavior for all types, not only enum types. + +We can also override it on a per type basis: + +```csharp +public class UserRoleType : EnumType +{ + protected override void Configure(IEnumTypeDescriptor descriptor) + { + descriptor.BindValues(BindingBehavior.Implicit); + + // We could also use the following methods respectively + // descriptor.BindValuesExplicitly(); + // descriptor.BindValuesImplicitly(); + } +} +``` + +## Ignoring values + + + + +In the implementation-first approach we can ignore values using the `[GraphQLIgnore]` attribute. + +```csharp +public enum UserRole +{ + [GraphQLIgnore] + Guest, + Standard, + Administrator +} +``` + + + + +In the code-first approach we can ignore values using the `Ignore` method on the `IEnumTypeDescriptor`. This is only necessary, if the binding behavior of the enum type is implicit. + +```csharp +public class UserRoleType : EnumType +{ + protected override void Configure(IEnumTypeDescriptor descriptor) + { + descriptor.Ignore(UserRole.Guest); + } +} +``` + + + + +We do not have to ignore values in the schema-first approach. + + + + +## Including values + +In the code-first approach we can explicitly include values using the `Value` method on the `IEnumTypeDescriptor`. This is only necessary, if the binding behavior of the enum type is explicit. + +```csharp +public class UserRoleType : EnumType +{ + protected override void Configure(IEnumTypeDescriptor descriptor) + { + descriptor.BindValuesExplicitly(); + + descriptor.Value(UserRole.Guest); + } +} +``` + +# Naming + +Unless specified explicitly, Hot Chocolate automatically infers the names of enums and their values. Per default the name of the enum becomes the name of the enum type. When using `EnumType` in code-first, the name of `T` is chosen as the name for the enum type. + +Enum values are automatically formatted to the UPPER_SNAKE_CASE according to the GraphQL specification: + +- `Guest` becomes `GUEST` +- `HeadOfDepartment` becomes `HEAD_OF_DEPARTMENT` + +If we need to we can override these inferred names. + + + + +The `[GraphQLName]` attribute allows us to specify an explicit name. + +```csharp +[GraphQLName("Role")] +public enum UserRole +{ + [GraphQLName("VISITOR")] + Guest, + Standard, + Administrator +} +``` + + + + +The `Name` method on the `IEnumTypeDescriptor` / `IEnumValueDescriptor` allows us to specify an explicit name. + +```csharp +public class UserRoleType : EnumType +{ + protected override void Configure(IEnumTypeDescriptor descriptor) + { + descriptor.Name("Role"); + + descriptor.Value(UserRole.Guest).Name("VISITOR"); + } +} +``` + + + + +Simply change the names in the schema. + + + + +This would produce the following `Role` schema enum type: + +```sdl +enum Role { + VISITOR, + STANDARD, + ADMINISTRATOR +} +``` diff --git a/website/src/docs/hotchocolate/v15/defining-a-schema/extending-types.md b/website/src/docs/hotchocolate/v15/defining-a-schema/extending-types.md new file mode 100644 index 00000000000..fcf7d7861b2 --- /dev/null +++ b/website/src/docs/hotchocolate/v15/defining-a-schema/extending-types.md @@ -0,0 +1,336 @@ +--- +title: "Extending Types" +--- + +Type extensions allow us to add, remove or replace fields on existing types, without necessarily needing access to these types. + +Because of these capabilities, they also allow for better organization of our types. We could for example have classes that encapsulate part of our domain and extend our `Query` type with these functionalities. + +Type extensions are especially useful if we want to modify third-party types, such as types that live in a separate assembly and are therefore not directly modifiable by us. + +