diff --git a/website/src/docs/docs.json b/website/src/docs/docs.json index 1a737d03387..07ed2ba4181 100644 --- a/website/src/docs/docs.json +++ b/website/src/docs/docs.json @@ -187,8 +187,8 @@ "title": "Introspection" }, { - "path": "uploading-files", - "title": "Uploading files" + "path": "files", + "title": "Files" }, { "path": "instrumentation", diff --git a/website/src/docs/hotchocolate/api-reference/migrate-from-11-to-12.md b/website/src/docs/hotchocolate/api-reference/migrate-from-11-to-12.md index 98a075423bd..4991c2e837e 100644 --- a/website/src/docs/hotchocolate/api-reference/migrate-from-11-to-12.md +++ b/website/src/docs/hotchocolate/api-reference/migrate-from-11-to-12.md @@ -4,6 +4,29 @@ title: Migrate from Hot Chocolate GraphQL server 11 to 12 This guide will walk you through the manual migration steps to get your Hot Chocolate GraphQL server to version 12. +# Resolvers + +We have reworked the resolver compiler and are now demanding that the `ParentAttribute` is used when an argument is referring to the parent object. +This is done since in some cases people want to get the parent object which is the same runtime type as an argument value. + +**v11** + +```csharp +public string MyResolver(Person parent, string additionalInput) +{ + // Code omitted for brevity +} +``` + +**v12** + +```csharp +public string MyResolver([Parent] Person parent, string additionalInput) +{ + // Code omitted for brevity +} +``` + # Scalars We changed some defaults around scalars. These new defaults can break your existing schema but are, in general, better for newcomers and align better with the overall GraphQL ecosystem. Of course, you can naturally opt out of these new defaults to preserve your current schema's integrity. @@ -52,7 +75,7 @@ services We have changed the way we infer the name for the connection type when using cursor-based pagination. By default, the connection name is now inferred from the field name instead of the type name. -```SDL +```sdl type Person { friends: [Person] } @@ -60,7 +83,7 @@ type Person { In version 11, we would have created a connection named `PersonConnection`. -```SDL +```sdl type Person { friends(first: Int, last: Int, after: String, before: String): PersonConnection } @@ -68,7 +91,7 @@ type Person { In version 12, we now will infer the connection name as `FriendsConnection`. -```SDL +```sdl type Person { friends(first: Int, last: Int, after: String, before: String): FriendsConnection } @@ -132,7 +155,7 @@ Therefore we introduced two separate APIs to give you more explicit control over ## Global Object Identification -**OLD** +**v11** ```csharp services @@ -140,7 +163,7 @@ services .EnableRelaySupport(); ``` -**NEW** +**v12** ```csharp services @@ -152,7 +175,7 @@ services ## Query field in Mutation payloads -**OLD** +**v11** ```csharp services @@ -165,7 +188,7 @@ services }); ``` -**NEW** +**v12** ```csharp sevices @@ -190,7 +213,7 @@ We have consolidated the DataLoader base classes into the GreenDonut package whi Second, we optimized memory usage of DataLoader and it is now best practice to let the DI inject the DataLoaderOptions into the DataLoader. -**Hot Chocolate 11** +**v11** ```csharp public class CustomBatchDataLoader : BatchDataLoader @@ -205,7 +228,7 @@ public class CustomBatchDataLoader : BatchDataLoader } ``` -**Hot Chocolate 12** +**v12** ```csharp public class CustomBatchDataLoader : BatchDataLoader @@ -221,15 +244,3 @@ public class CustomBatchDataLoader : BatchDataLoader ``` Allowing the DI to inject the options will allow the DataLoader to use the new shared pooled cache objects. - -# Resolvers - -We have reworked the resolver compiler and are now demanding that the `ParentAttribute` is used when an argument is referring to the parent object. -This is done since in some cases people want to get the parent object which is the same runtime type as an argument value. - -```csharp -public async Task MyResolver([Parent] Person parent, Person input) -{ - // code omitted for brevity. -} -``` diff --git a/website/src/docs/hotchocolate/defining-a-schema/non-null.md b/website/src/docs/hotchocolate/defining-a-schema/non-null.md index cc6b970819a..ad1076488de 100644 --- a/website/src/docs/hotchocolate/defining-a-schema/non-null.md +++ b/website/src/docs/hotchocolate/defining-a-schema/non-null.md @@ -101,9 +101,49 @@ public class BookType : ObjectType ```sdl -type User { - name: String! - nullableName: String +type Book { + title: String! + nullableTitle: String +} +``` + + + + +The inner type of a list can be made non-null like the following. + + + + +```csharp +public class Book +{ + [GraphQLType(typeof(ListType>))] + public List Genres { get; set; } +} +``` + + + + +```csharp +public class BookType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .Field(f => f.Genres) + .Type>>(); + } +} +``` + + + + +```sdl +type Book { + genres: [String!] } ``` diff --git a/website/src/docs/hotchocolate/performance/index.md b/website/src/docs/hotchocolate/performance/index.md index 129393eb480..eb98fa3b90e 100644 --- a/website/src/docs/hotchocolate/performance/index.md +++ b/website/src/docs/hotchocolate/performance/index.md @@ -2,16 +2,38 @@ title: "Overview" --- -**Improve performance by sending smaller requests and pre-compile queries** +In this section we will look at some ways of how we can improve the performance of our Hot Chocolate GraphQL server. -The size of individual GraphQL requests can become a major pain point. This is not only true for the transport but also introduces inefficiencies for the server since large requests need to be parsed and validated. To solve this problem, Hot Chocolate has implemented persisted queries. With persisted queries, we can store queries on the server in a key-value store. When we want to execute a persisted query, we can send the key under which the query is stored instead of the query itself. This saves precious bandwidth and also improves execution time since the server will validate, parse, and compile persisted queries just once. +# Startup performance + +The first GraphQL request issued against a Hot Chocolate server will most of the time take a little longer than subsequent requests. This is because Hot Chocolate has to build up the GraphQL schema and prepare for the execution of requests. + +We can however delegate this task to the startup of the application instead of the first request, by call `InitializeOnStartup()` on the `IRequestExecutorBuilder`. + +```csharp +public class Startup +{ + public void ConfigureServices(IServiceCollection services) + { + services + .AddGraphQLServer() + .InitializeOnStartup() + } +} +``` + +This will create the schema and warmup the request executor as soon as the app starts. This also brings the added benefit that schema errors are surfaced at app startup and not on the first request. + +# Persisted queries + +The size of individual GraphQL requests can become a major pain point. This is not only true for the transport but also the server, since large requests need to be parsed and validated often. To solve this problem, Hot Chocolate implements persisted queries. With persisted queries, we can store queries on the server in a key-value store. When we want to execute a persisted query, we can send the key under which the query is stored instead of the query itself. This saves precious bandwidth and also improves execution time since the server will validate, parse, and compile persisted queries just once. Hot Chocolate supports two flavors of persisted queries. -## Persisted queries +## Regular persisted queries The first approach is to store queries ahead of time (ahead of deployment). -This can be done by extracting the queries from your client application at build time. This will reduce the size of the requests and the bundle size of your application since queries can be removed from the client code at build time and are replaced with query hashes. Apart from performance, persisted queries can also be used for security by configuring Hot Chocolate to only accept persisted queries on production. +This can be done by extracting the queries from our client applications at build time. This will reduce the size of the requests and the bundle size of our application since queries can be removed from the client code at build time and are replaced with query hashes. Strawberry Shake, [Relay](https://relay.dev/docs/guides/persisted-queries/), and [Apollo](https://www.apollographql.com/docs/react/api/link/persisted-queries/) client all support this approach. @@ -19,9 +41,9 @@ Strawberry Shake, [Relay](https://relay.dev/docs/guides/persisted-queries/), and ## Automatic persisted queries -Automatic persisted queries allow us to store queries dynamically on the server at runtime. With this approach, we can give our application the same performance benefits as with persisted queries without having to opt in to a more complex build process. +Automatic persisted queries allow us to store queries dynamically on the server at runtime. With this approach, we can give our applications the same performance benefits as with persisted queries without having to opt in to a more complex build process. -However, we do not have the option to seal our server from queries that we do not know, so this approach has **no** security benefits. We do not have any bundle size improvements for our application since the query is still needed at runtime. +However, we do not get any bundle size improvements for our applications since the queries are still needed at runtime. Both Strawberry Shake and [Apollo](https://www.apollographql.com/docs/apollo-server/performance/apq/) client support this approach. diff --git a/website/src/docs/hotchocolate/performance/persisted-queries.md b/website/src/docs/hotchocolate/performance/persisted-queries.md index 4365458434f..c94f220daad 100644 --- a/website/src/docs/hotchocolate/performance/persisted-queries.md +++ b/website/src/docs/hotchocolate/performance/persisted-queries.md @@ -2,9 +2,11 @@ title: "Persisted queries" --- -Persisted queries allow us to pre-register all required queries of our clients. This can be done by extracting the queries from our client applications at build time and putting them in the server's query storage. Extracting queries is supported by client libraries like [Relay](https://relay.dev/docs/guides/persisted-queries/) and in the case of [Strawberry Shake](/docs/strawberryshake) we do not have to do any additional work. +Persisted queries allow us to pre-register all required queries of our clients. This can be done by extracting the queries of our client applications at build time and placing them in the server's query storage. -> Note: While this is called persisted _queries_ it works for all other GraphQL operations as well. +Extracting queries is supported by client libraries like [Relay](https://relay.dev/docs/guides/persisted-queries/) and in the case of [Strawberry Shake](/docs/strawberryshake) we do not have to do any additional work. + +> Note: While this feature is called persisted _queries_ it works for all other GraphQL operations as well. # How it works diff --git a/website/src/docs/hotchocolate/server/files.md b/website/src/docs/hotchocolate/server/files.md new file mode 100644 index 00000000000..0fd95907300 --- /dev/null +++ b/website/src/docs/hotchocolate/server/files.md @@ -0,0 +1,307 @@ +--- +title: Files +--- + +import { ExampleTabs } from "../../../components/mdx/example-tabs"; + +Handling files is traditionally not a concern of a GraphQL server, which is also why the [GraphQL over HTTP](https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md) specification doesn't mention it. + +That being said, we recognize that at some point in the development of a new application you'll likely have to deal with files in some way or another. Which is why we want to give you some guidance on this topic. + +# Uploading files + +When it comes to uploading files there are a couple of options we have. + +## Completely decoupled + +We could handle file uploads completely decoupled from our GraphQL server, for example using a dedicated web application offering a HTTP endpoint for us to upload our files to. + +This however has a couple of downsides: + +- Authentication and authorization need to be handled by this dedicated endpoint as well. +- The process of uploading a file would need to be documented outside of our GraphQL schema. + +## Upload scalar + +Hot Chocolate implements the [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec) which adds a new `Upload` scalar and allows our GraphQL server to handle file upload streams. + +> ⚠️ Note: Files can not yet be uploaded through a gateway to stitched services using the `Upload` scalar. + +### Usage + +In order to use file upload streams in our input types or as an argument register the `Upload` scalar like the following: + +```csharp +services + .AddGraphQLServer() + .AddType(); +``` + +> Note: The `Upload` scalar can only be used as an input type and does not work on output types. + +We can use the `Upload` scalar as an argument like the following: + + + + +```csharp +public class Mutation +{ + public async Task UploadFileAsync(IFile file) + { + var fileName = file.Name; + var fileSize = file.Length; + + await using Stream stream = file.OpenReadStream(); + + // We can now work with standard stream functionality of .NET + // to handle the file. + } +} +``` + + + + +```csharp +public class MutationType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .Field("uploadFile") + .Argument("file", a => a.Type()) + .Resolve(async context => + { + var file = context.ArgumentValue("file"); + + var fileName = file.Name; + var fileSize = file.Length; + + await using Stream stream = file.OpenReadStream(); + + // We can now work with standard stream functionality of .NET + // to handle the file. + }); + } +} +``` + + + + +Take a look at the Annotation-based or Code-first example. + + + + +[Learn more about arguments](/docs/hotchocolate/defining-a-schema/arguments) + +In input object types it can be used like the following. + + + + +```csharp +public class ExampleInput +{ + [GraphQLType(typeof(NonNullType))] + public IFile File { get; set; } +} +``` + + + + +```csharp +public class ExampleInput +{ + public IFile File { get; set; } +} + +public class ExampleInputType : InputObjectType +{ + protected override void Configure(IInputObjectTypeDescriptor descriptor) + { + descriptor.Field(f => f.File).Type(); + } +} +``` + + + + +Take a look at the Annotation-based or Code-first example. + + + + +[Learn more about input object types](/docs/hotchocolate/defining-a-schema/input-object-types) + +If you need to upload a list of files, it works exactly as you would expect. You just use a `List` or `ListType`. + +[Learn more about lists](/docs/hotchocolate/defining-a-schema/lists) + +### Client usage + +When performing a mutation with the `Upload` scalar, we need to use variables. + +An example mutation could look like the following: + +```graphql +mutation ($file: Upload!) { + uploadFile(file: $file) { + success + } +} +``` + +If we now want to send this request to our GraphQL server, we need to do so using HTTP multipart: + +```bash +curl localhost:5000/graphql \ + -F operations='{ "query": "mutation ($file: Upload!) { uploadFile(file: $file) { success } }", "variables": { "file": null } }' \ + -F map='{ "0": ["variables.file"] }' \ + -F 0=@file.txt + +``` + +> Note: The `$file` variable is intentionally `null`. It is filled in by Hot Chocolate on the server. + +[More examples can be found here](https://github.com/jaydenseric/graphql-multipart-request-spec#examples) + +You can check if your GraphQL client supports the specification [here](https://github.com/jaydenseric/graphql-multipart-request-spec#client). + +Both Relay and Apollo support this specification through community packages: + +- [react-relay-network-modern](https://github.com/relay-tools/react-relay-network-modern) using the `uploadMiddleware` +- [apollo-upload-client](https://github.com/jaydenseric/apollo-upload-client) + +> ⚠️ Note: [Strawberry Shake](/docs/strawberryshake) does not yet support the `Upload` scalar. + +### Options + +If you need to upload larger files or set custom upload size limits, you can configure those by registering custom [`FormOptions`](https://docs.microsoft.com/dotnet/api/microsoft.aspnetcore.http.features.formoptions). + +```csharp +services.Configure(options => +{ + // Set the limit to 256 MB + options.MultipartBodyLengthLimit = 268435456; +}); +``` + +Based on our WebServer we might need to configure these limits elsewhere as well. [Kestrel](https://docs.microsoft.com/aspnet/core/mvc/models/file-uploads#kestrel-maximum-request-body-size) and [IIS](https://docs.microsoft.com/aspnet/core/mvc/models/file-uploads#iis) are covered in the ASP.NET Core Documentation. + +## Presigned upload URLs + +The arguably best solution for uploading files is a hybrid of the above. Our GraphQL server still provides a mutation for uploading files, **but** the mutation is only used to setup a file upload. The actual file upload is done through a dedicated endpoint. + +We can accomplish this by returning _presigned upload URLs_ from our mutations. _Presigned upload URLs_ are URLs that point to an endpoint, through which we can upload our files. Files can only be uploaded to this endpoint, if the URL to this endpoint contains a valid token. Our mutation generates said token, appends the token to the upload URL and returns the _presigned_ URL to the client. + +Let's take a look at a quick example. We have built the following mutation resolver: + +```csharp +public record ProfilePictureUploadPayload(string UploadUrl); + +public class Mutation +{ + [Authorize] + public ProfilePictureUploadPayload UploadProfilePicture() + { + var baseUrl = "https://blob.chillicream.com/upload"; + + // Here we can handle our authorization logic + + // If the user is allowed to upload the profile picture + // we generate the token + var token = "myuploadtoken"; + + var uploadUrl = QueryHelpers.AddQueryString(baseUrl, "token", token); + + return new(uploadUrl); + } +} +``` + +If you are using any of the big cloud providers for storing your BLOBs, chances are they already come with support for _presigned upload URLs_: + +- [Azure Storage shared access signatures](https://docs.microsoft.com/en-us/azure/storage/common/storage-sas-overview) +- [AWS presigned URLS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/PresignedUrlUploadObject.html) +- [GCP signed URLs](https://cloud.google.com/storage/docs/access-control/signed-urls) + +If you need to implement the file upload endpoint yourself, you can research best practices for creating _presigned upload URLs_. + +Let's take a look at how a client would upload a new profile picture. + +**Request** + +```graphql +mutation { + uploadProfilePicture { + uploadUrl + } +} +``` + +**Response** + +```json +{ + "data": { + "uploadProfilePicture": { + "uploadUrl": "https://blob.chillicream.com/upload?token=myuploadtoken" + } + } +} +``` + +Given the `uploadUrl` our client can now HTTP POST the file to this endpoint to upload his profile picture. + +This solution offers the following benefits: + +- Uploading files is treated as a separate concern and our GraphQL server is kept _pure_ in a sense. +- The GraphQL server maintains control over authorization and all of the business logic regarding granting a file upload stays in one place. +- The action of uploading a profile picture is described by the schema and therefore more discoverable for developers. + +There is still some uncertainty about how the actual file upload happens, e.g. which HTTP verb to use or which headers to send using the `uploadUrl`. These additional parameters can either be documented somewhere or be made queryable using our mutation. + +# Serving files + +Let's imagine we want to expose the file we just uploaded as the user's profile picture. How would we query for this file? + +We _could_ make the profile picture a queryable field in our graph that returns the Base64 encoded image. While this _can_ work it has a number of downsides: + +- Since the image is part of the JSON serialized GraphQL response, caching is incredibly hard. +- A query for the user's name might take a couple of milliseconds to transfer from the server to the client. Additionally querying for the image data might increase the response time by seconds. +- Let's not even think about how video playback, i.e. streaming, would work... + +The recommended solution is to serve files through a different HTTP endpoint and only referencing this endpoint in our GraphQL response. So instead of querying for the profile picture we would query for an URL that points to the profile picture. + +**Request** + +```graphql +{ + user { + name + imageUrl + } +} +``` + +**Response** + +```json +{ + "data": { + "user": { + "name": "John Doe", + "imageUrl": "https://blob.chillicream.com/john-doe.png" + } + } +} +``` + +Serving the file through a dedicated HTTP endpoint makes caching a lot easier and also allows for features like streaming video. Ultimately it gives control to the client on how a resource should be handled, given its URL. In the case of a web application we can pass the `imageUrl` as `src` to a HTML `img` element and let the browser handle the fetching and caching of the image. + +If you are using a cloud provider for file storage, chances are you are already accessing the files using an URL and you can simply expose this URL as a `String` field in your graph. If infrastructure for serving files is not already in place, you can look into how files can be served using ASP.NET Core or how to setup a dedicated web server like nginx to serve the files. diff --git a/website/src/docs/hotchocolate/server/index.md b/website/src/docs/hotchocolate/server/index.md index 3bf7c3ab164..88a91a7a675 100644 --- a/website/src/docs/hotchocolate/server/index.md +++ b/website/src/docs/hotchocolate/server/index.md @@ -30,11 +30,11 @@ Introspection allows us to query the type system of our GraphQL server using reg [Learn more about introspection](/docs/hotchocolate/server/introspection) -# Uploading files +# Files -Though not considered one of the responsibilities of a GraphQL server, for convenience, Hot Chocolate provides file upload support. +Though not considered one of the responsibilities of a GraphQL server, for convenience, Hot Chocolate provides file upload support. We will also take a look at what other options we have when it comes to uploading and serving files. -[Learn more about uploading files](/docs/hotchocolate/server/uploading-files) +[Learn more about handling files](/docs/hotchocolate/server/files) # Instrumentation diff --git a/website/src/docs/hotchocolate/server/middleware.md b/website/src/docs/hotchocolate/server/middleware.md index 25b16e7bc4b..39ee9ac5820 100644 --- a/website/src/docs/hotchocolate/server/middleware.md +++ b/website/src/docs/hotchocolate/server/middleware.md @@ -117,7 +117,7 @@ endpoints.MapGraphQL().WithOptions(new GraphQLServerOptions This setting controls whether the GraphQL server is able to handle HTTP Multipart forms, i.e. file uploads. -[Learn more about uploading files](/docs/hotchocolate/server/uploading-files) +[Learn more about uploading files](/docs/hotchocolate/server/files#upload-scalar) ### Tool diff --git a/website/src/docs/hotchocolate/server/uploading-files.md b/website/src/docs/hotchocolate/server/uploading-files.md deleted file mode 100644 index cb36b8473aa..00000000000 --- a/website/src/docs/hotchocolate/server/uploading-files.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: Uploading files ---- - -Hot Chocolate implements the GraphQL multipart request specification which allows it to handle file upload streams. - -[Learn more about the specification](https://github.com/jaydenseric/graphql-multipart-request-spec) - -# Usage - -In order to use file upload streams in our input types or as an argument register the `Upload` scalar like the following. - -```csharp -services - .AddGraphQLServer() - .AddType(); -``` - -In our resolver or input type we can then use the `IFile` interface to use the upload scalar. - -```csharp -public class Mutation -{ - public async Task UploadFileAsync(IFile file) - { - using Stream stream = file.OpenReadStream(); - // we can now work with standard stream functionality of .NET - // to handle the file. - } - - public async Task UploadFiles(List files) - { - // Omitted code for brevity - } - - public async Task UploadFileInInput(ExampleInput input) - { - // Omitted code for brevity - } -} - -public class ExampleInput -{ - [GraphQLType(typeof(NonNullType))] - public IFile File { get; set; } -} -``` - -> Note: The `Upload` scalar can only be used as an input type and does not work on output types. - -# Configuration - -If we need to upload large files or set custom upload size limits, we can configure those by registering custom [`FormOptions`](https://docs.microsoft.com/dotnet/api/microsoft.aspnetcore.http.features.formoptions). - -```csharp -services.Configure(options => -{ - // Set the limit to 256 MB - options.MultipartBodyLengthLimit = 268435456; -}); -``` - -Based on our WebServer we might need to configure these limits elsewhere as well. [Kestrel](https://docs.microsoft.com/aspnet/core/mvc/models/file-uploads#kestrel-maximum-request-body-size) and [IIS](https://docs.microsoft.com/aspnet/core/mvc/models/file-uploads#iis) are covered in the ASP.NET Core Documentation.