├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── branches.yml │ ├── master.yml │ └── publish.yml ├── .gitignore ├── Directory.Build.props ├── Directory.Build.targets ├── GitVersion.yml ├── GraphQL.Client.sln ├── GraphQL.Client.sln.DotSettings ├── LICENSE.txt ├── README.md ├── SubscriptionIntegrationTest.ConsoleClient ├── Program.cs └── SubscriptionIntegrationTest.ConsoleClient.csproj ├── assets ├── logo.64x64.png └── logo.svg ├── dotnet-tools.json ├── examples ├── .editorconfig └── GraphQL.Client.Example │ ├── GraphQL.Client.Example.csproj │ ├── PersonAndFilmsResponse.cs │ └── Program.cs ├── src ├── GraphQL.Client.Abstractions.Websocket │ ├── GraphQL.Client.Abstractions.Websocket.csproj │ ├── GraphQLWebSocketMessageType.cs │ ├── GraphQLWebSocketRequest.cs │ ├── GraphQLWebSocketResponse.cs │ ├── GraphQLWebsocketConnectionState.cs │ ├── IGraphQLWebSocketClient.cs │ ├── IGraphQLWebsocketJsonSerializer.cs │ └── WebsocketMessageWrapper.cs ├── GraphQL.Client.Abstractions │ ├── GraphQL.Client.Abstractions.csproj │ ├── GraphQLClientExtensions.cs │ ├── GraphQLJsonSerializerExtensions.cs │ ├── IGraphQLClient.cs │ ├── IGraphQLJsonSerializer.cs │ └── Utilities │ │ ├── StringExtensions.cs │ │ └── StringUtils.cs ├── GraphQL.Client.LocalExecution │ ├── GraphQL.Client.LocalExecution.csproj │ ├── GraphQLLocalExecutionClient.cs │ └── ServiceCollectionExtensions.cs ├── GraphQL.Client.Serializer.Newtonsoft │ ├── ConstantCaseEnumConverter.cs │ ├── GraphQL.Client.Serializer.Newtonsoft.csproj │ ├── MapConverter.cs │ └── NewtonsoftJsonSerializer.cs ├── GraphQL.Client.Serializer.SystemTextJson │ ├── ConstantCaseJsonNamingPolicy.cs │ ├── ConverterHelperExtensions.cs │ ├── ErrorPathConverter.cs │ ├── GraphQL.Client.Serializer.SystemTextJson.csproj │ ├── ImmutableConverter.cs │ ├── JsonSerializerOptionsExtensions.cs │ ├── MapConverter.cs │ └── SystemTextJsonSerializer.cs ├── GraphQL.Client │ ├── GraphQL.Client.csproj │ ├── GraphQLHttpClient.cs │ ├── GraphQLHttpClientExtensions.cs │ ├── GraphQLHttpClientOptions.cs │ ├── GraphQLHttpRequest.cs │ ├── GraphQLHttpRequestException.cs │ ├── GraphQLHttpResponse.cs │ ├── GraphQLSubscriptionException.cs │ ├── UriExtensions.cs │ └── Websocket │ │ ├── GraphQLHttpWebSocket.cs │ │ ├── GraphQLTransportWSProtocolHandler.cs │ │ ├── GraphQLWSProtocolHandler.cs │ │ ├── GraphQLWebsocketConnectionException.cs │ │ ├── IWebsocketProtocolHandler.cs │ │ └── WebSocketProtocols.cs └── GraphQL.Primitives │ ├── ErrorPath.cs │ ├── GraphQL.Primitives.csproj │ ├── GraphQLError.cs │ ├── GraphQLLocation.cs │ ├── GraphQLQuery.cs │ ├── GraphQLRequest.cs │ ├── GraphQLResponse.cs │ ├── Hash.cs │ ├── IGraphQLResponse.cs │ ├── Map.cs │ └── StringSyntaxAttribute.cs └── tests ├── .editorconfig ├── GraphQL.Client.Serializer.Tests ├── BaseSerializeNoCamelCaseTest.cs ├── BaseSerializerTest.cs ├── ConsistencyTests.cs ├── DefaultValidationTest.cs ├── GraphQL.Client.Serializer.Tests.csproj ├── NewtonsoftSerializerTest.cs ├── SystemTextJsonSerializerTests.cs └── TestData │ ├── DeserializeResponseTestData.cs │ ├── SerializeToBytesTestData.cs │ └── SerializeToStringTestData.cs ├── GraphQL.Client.Tests.Common ├── Chat │ ├── AddMessageMutationResult.cs │ ├── AddMessageVariables.cs │ ├── GraphQLClientChatExtensions.cs │ ├── JoinDeveloperMutationResult.cs │ └── Schema │ │ ├── CapitalizedFieldsGraphType.cs │ │ ├── ChatMutation.cs │ │ ├── ChatQuery.cs │ │ ├── ChatSchema.cs │ │ ├── ChatSubscriptions.cs │ │ ├── IChat.cs │ │ ├── Message.cs │ │ ├── MessageFrom.cs │ │ ├── MessageFromType.cs │ │ ├── MessageType.cs │ │ └── ReceivedMessage.cs ├── Common.cs ├── GraphQL.Client.Tests.Common.csproj ├── Helpers │ ├── AvailableJsonSerializers.cs │ ├── CallbackMonitor.cs │ ├── ConcurrentTaskWrapper.cs │ ├── MiscellaneousExtensions.cs │ └── NetworkHelpers.cs ├── Properties │ └── launchSettings.json └── StarWars │ ├── Extensions │ └── ResolveFieldContextExtensions.cs │ ├── StarWarsData.cs │ ├── StarWarsMutation.cs │ ├── StarWarsQuery.cs │ ├── StarWarsSchema.cs │ ├── TestData │ └── StarWarsHumans.cs │ └── Types │ ├── CharacterInterface.cs │ ├── DroidType.cs │ ├── EpisodeEnum.cs │ ├── HumanInputType.cs │ ├── HumanType.cs │ └── StarWarsCharacter.cs ├── GraphQL.Integration.Tests ├── APQ │ └── AutomaticPersistentQueriesTest.cs ├── GraphQL.Integration.Tests.csproj ├── Helpers │ ├── IntegrationServerTestFixture.cs │ └── WebHostHelpers.cs ├── Properties │ └── launchSettings.json ├── QueryAndMutationTests │ ├── Base.cs │ ├── Newtonsoft.cs │ └── SystemTextJson.cs ├── UriExtensionTests.cs ├── UserAgentHeaderTests.cs └── WebsocketTests │ ├── Base.cs │ ├── NewtonsoftGraphQLTransportWs.cs │ ├── NewtonsoftGraphQLWs.cs │ ├── SystemTextJsonAutoNegotiate.cs │ ├── SystemTextJsonGraphQLTransportWs.cs │ └── SystemTextJsonGraphQLWs.cs ├── GraphQL.Primitives.Tests ├── GraphQL.Primitives.Tests.csproj ├── GraphQLLocationTest.cs ├── GraphQLRequestTest.cs ├── GraphQLResponseTest.cs └── JsonSerializationTests.cs ├── GraphQL.Server.Test ├── GraphQL.Server.Test.csproj ├── GraphQL │ ├── Models │ │ └── Repository.cs │ ├── Storage.cs │ ├── TestMutation.cs │ ├── TestQuery.cs │ ├── TestSchema.cs │ └── TestSubscription.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── appsettings.Development.json ├── appsettings.json └── libman.json ├── IntegrationTestServer ├── IntegrationTestServer.csproj ├── Program.cs ├── Properties │ └── launchSettings.json └── Startup.cs └── tests.props /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "nuget" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /.github/workflows/branches.yml: -------------------------------------------------------------------------------- 1 | name: Branch workflow 2 | on: 3 | push: 4 | branches-ignore: 5 | - master 6 | - 'release/**' 7 | - 'releases/**' 8 | pull_request: 9 | 10 | env: 11 | DOTNET_NOLOGO: true 12 | DOTNET_CLI_TELEMETRY_OPTOUT: true 13 | MSBUILDSINGLELOADCONTEXT: 1 14 | 15 | jobs: 16 | build: 17 | name: Build and Test 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | - name: Setup .NET SDK 25 | uses: actions/setup-dotnet@v4 26 | with: 27 | dotnet-version: | 28 | 8.0.x 29 | - name: Restore dotnet tools 30 | run: dotnet tool restore 31 | - name: Fetch complete repository including tags 32 | run: git fetch --tags --force --prune && git describe 33 | - name: Generate version info from git history 34 | run: dotnet gitversion /output json | jq -r 'to_entries|map("GitVersion_\(.key)=\(.value|tostring)")|.[]' >> $GITHUB_ENV 35 | - name: Fail if the version number has not been resolved 36 | if: ${{ !env.GitVersion_SemVer }} 37 | run: | 38 | echo Error! Version number not resolved! 39 | exit 1 40 | - name: Print current version 41 | run: echo "Current version is \"$GitVersion_SemVer\"" 42 | - name: Install dependencies 43 | run: dotnet restore 44 | - name: Build solution 45 | run: dotnet build --no-restore -c Release 46 | - name: Run Tests 47 | run: dotnet test -c Release --no-restore --no-build 48 | - name: Create NuGet packages 49 | run: dotnet pack -c Release --no-restore --no-build -o nupkg 50 | - name: Upload nuget packages 51 | uses: actions/upload-artifact@v4 52 | with: 53 | name: nupkg 54 | path: nupkg 55 | -------------------------------------------------------------------------------- /.github/workflows/master.yml: -------------------------------------------------------------------------------- 1 | name: Master workflow 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - 'release/**' 7 | - 'releases/**' 8 | 9 | env: 10 | DOTNET_NOLOGO: true 11 | DOTNET_CLI_TELEMETRY_OPTOUT: true 12 | MSBUILDSINGLELOADCONTEXT: 1 13 | 14 | jobs: 15 | build: 16 | name: Build and Test 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | - name: Setup .NET SDK 24 | uses: actions/setup-dotnet@v4 25 | with: 26 | dotnet-version: "8.0.x" 27 | source-url: https://nuget.pkg.github.com/graphql-dotnet/index.json 28 | env: 29 | NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 30 | - name: Restore dotnet tools 31 | run: dotnet tool restore 32 | - name: Fetch complete repository including tags 33 | run: git fetch --tags --force --prune && git describe 34 | - name: Generate version info from git history 35 | run: dotnet gitversion /output json | jq -r 'to_entries|map("GitVersion_\(.key)=\(.value|tostring)")|.[]' >> $GITHUB_ENV 36 | - name: Fail if the version number has not been resolved 37 | if: ${{ !env.GitVersion_SemVer }} 38 | run: | 39 | echo Error! Version number not resolved! 40 | exit 1 41 | - name: Print current version 42 | run: echo "Current version is \"$GitVersion_SemVer\"" 43 | - name: Install dependencies 44 | run: dotnet restore 45 | - name: Build solution 46 | run: dotnet build --no-restore -c Release 47 | - name: Run Tests 48 | run: dotnet test -c Release --no-restore --no-build 49 | - name: Create NuGet packages 50 | run: dotnet pack -c Release --no-restore --no-build -o nupkg 51 | - name: Upload nuget packages as artifacts 52 | uses: actions/upload-artifact@v4 53 | with: 54 | name: nupkg 55 | path: nupkg 56 | - name: Publish Nuget packages to GitHub registry 57 | run: dotnet nuget push "nupkg/*" -k ${{secrets.GITHUB_TOKEN}} 58 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | release: 4 | types: 5 | - published 6 | 7 | env: 8 | DOTNET_NOLOGO: true 9 | DOTNET_CLI_TELEMETRY_OPTOUT: true 10 | MSBUILDSINGLELOADCONTEXT: 1 11 | 12 | jobs: 13 | build: 14 | name: Build and Test 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | - name: Check github.ref starts with 'refs/tags/' 22 | if: ${{ !startsWith(github.ref, 'refs/tags/') }} 23 | run: | 24 | echo Error! github.ref does not start with 'refs/tags' 25 | echo github.ref: ${{ github.ref }} 26 | exit 1 27 | - name: Setup .NET SDK 28 | uses: actions/setup-dotnet@v4 29 | with: 30 | dotnet-version: "8.0.x" 31 | source-url: https://api.nuget.org/v3/index.json 32 | env: 33 | NUGET_AUTH_TOKEN: ${{secrets.NUGET_API_KEY}} 34 | - name: Restore dotnet tools 35 | run: dotnet tool restore 36 | - name: Fetch complete repository including tags 37 | run: git fetch --tags --force --prune && git describe 38 | - name: Generate version info from git history 39 | run: dotnet gitversion /output json | jq -r 'to_entries|map("GitVersion_\(.key)=\(.value|tostring)")|.[]' >> $GITHUB_ENV 40 | - name: Fail if the version number has not been resolved 41 | if: ${{ !env.GitVersion_SemVer }} 42 | run: | 43 | echo Error! Version number not resolved! 44 | exit 1 45 | - name: Print current version 46 | run: echo "Current version is \"$GitVersion_SemVer\"" 47 | - name: Install dependencies 48 | run: dotnet restore 49 | - name: Build solution 50 | run: dotnet build --no-restore -c Release 51 | - name: Create NuGet packages 52 | run: dotnet pack -c Release --no-restore --no-build -o nupkg 53 | - name: Upload nuget packages as artifacts 54 | uses: actions/upload-artifact@v4 55 | with: 56 | name: nupkg 57 | path: nupkg 58 | - name: Publish Nuget packages to Nuget registry 59 | run: dotnet nuget push "nupkg/*" -k ${{secrets.NUGET_API_KEY}} 60 | - name: Upload Nuget packages as release artifacts 61 | uses: actions/github-script@v7 62 | with: 63 | github-token: ${{secrets.GITHUB_TOKEN}} 64 | script: | 65 | console.log('environment', process.versions); 66 | const fs = require('fs').promises; 67 | const { repo: { owner, repo }, sha } = context; 68 | for (let file of await fs.readdir('nupkg')) { 69 | console.log('uploading', file); 70 | await github.rest.repos.uploadReleaseAsset({ 71 | owner, 72 | repo, 73 | release_id: ${{ github.event.release.id }}, 74 | name: file, 75 | data: await fs.readFile(`nupkg/${file}`) 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vs/ 3 | .vscode/ 4 | bin/ 5 | obj/ 6 | *.user 7 | nuget/ -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Deinok,Alexander Rose,graphql-dotnet 5 | A GraphQL Client for .NET Standard 6 | true 7 | true 8 | latest 9 | en-US 10 | $(NoWarn);NU5105 11 | $(NoWarn);1591 12 | annotations 13 | logo.64x64.png 14 | MIT 15 | https://github.com/graphql-dotnet/graphql-client 16 | true 17 | GraphQL 18 | git 19 | true 20 | true 21 | 22 | 23 | true 24 | embedded 25 | enable 26 | true 27 | true 28 | True 29 | 4 30 | true 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | README.md 5 | 6 | true 7 | 8 | 9 | 27 | 28 | 29 | $(NoWarn);1591 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | all 39 | runtime; build; native; contentfiles; analyzers 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /GitVersion.yml: -------------------------------------------------------------------------------- 1 | mode: ContinuousDeployment 2 | branches: 3 | master: 4 | tag: alpha 5 | -------------------------------------------------------------------------------- /GraphQL.Client.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | APQ 3 | QL -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 graphql-dotnet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /SubscriptionIntegrationTest.ConsoleClient/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.WebSockets; 3 | using System.Reactive.Disposables; 4 | using System.Reactive.Linq; 5 | using System.Threading.Tasks; 6 | using GraphQL.Client.Http; 7 | using GraphQL.Common.Request; 8 | 9 | namespace SubsccriptionIntegrationTest.ConsoleClient 10 | { 11 | class Program 12 | { 13 | static async Task Main(string[] args) 14 | { 15 | Console.WriteLine("configuring client ..."); 16 | using (var client = new GraphQLHttpClient("http://localhost:5000/graphql/", new GraphQLHttpClientOptions{ UseWebSocketForQueriesAndMutations = true })) 17 | { 18 | 19 | Console.WriteLine("subscribing to message stream ..."); 20 | 21 | var subscriptions = new CompositeDisposable(); 22 | 23 | subscriptions.Add(client.WebSocketReceiveErrors.Subscribe(e => { 24 | if(e is WebSocketException we) 25 | Console.WriteLine($"WebSocketException: {we.Message} (WebSocketError {we.WebSocketErrorCode}, ErrorCode {we.ErrorCode}, NativeErrorCode {we.NativeErrorCode}"); 26 | else 27 | Console.WriteLine($"Exception in websocket receive stream: {e.ToString()}"); 28 | })); 29 | 30 | subscriptions.Add(CreateSubscription("1", client)); 31 | await Task.Delay(200); 32 | subscriptions.Add(CreateSubscription2("2", client)); 33 | await Task.Delay(200); 34 | subscriptions.Add(CreateSubscription("3", client)); 35 | await Task.Delay(200); 36 | subscriptions.Add(CreateSubscription("4", client)); 37 | await Task.Delay(200); 38 | subscriptions.Add(CreateSubscription("5", client)); 39 | await Task.Delay(200); 40 | subscriptions.Add(CreateSubscription("6", client)); 41 | await Task.Delay(200); 42 | subscriptions.Add(CreateSubscription("7", client)); 43 | 44 | using (subscriptions) 45 | { 46 | Console.WriteLine("client setup complete"); 47 | var quit = false; 48 | do 49 | { 50 | Console.WriteLine("write message and press enter..."); 51 | var message = Console.ReadLine(); 52 | var graphQLRequest = new GraphQLRequest(@" 53 | mutation($input: MessageInputType){ 54 | addMessage(message: $input){ 55 | content 56 | } 57 | }") 58 | { 59 | Variables = new 60 | { 61 | input = new 62 | { 63 | fromId = "2", 64 | content = message, 65 | sentAt = DateTime.Now 66 | } 67 | } 68 | }; 69 | var result = await client.SendMutationAsync(graphQLRequest).ConfigureAwait(false); 70 | 71 | if(result.Errors != null && result.Errors.Length > 0) 72 | { 73 | Console.WriteLine($"request returned {result.Errors.Length} errors:"); 74 | foreach (var item in result.Errors) 75 | { 76 | Console.WriteLine($"{item.Message}"); 77 | } 78 | } 79 | } 80 | while(!quit); 81 | Console.WriteLine("shutting down ..."); 82 | } 83 | Console.WriteLine("subscriptions disposed ..."); 84 | } 85 | Console.WriteLine("client disposed ..."); 86 | } 87 | 88 | private static IDisposable CreateSubscription(string id, GraphQLHttpClient client) 89 | { 90 | #pragma warning disable 618 91 | var stream = client.CreateSubscriptionStream(new GraphQLRequest(@" 92 | subscription { 93 | messageAdded{ 94 | content 95 | from { 96 | displayName 97 | } 98 | } 99 | }" 100 | ) 101 | { Variables = new { id } }); 102 | #pragma warning restore 618 103 | 104 | return stream.Subscribe( 105 | response => Console.WriteLine($"{id}: new message from \"{response.Data.messageAdded.from.displayName.Value}\": {response.Data.messageAdded.content.Value}"), 106 | exception => Console.WriteLine($"{id}: message subscription stream failed: {exception}"), 107 | () => Console.WriteLine($"{id}: message subscription stream completed")); 108 | 109 | } 110 | 111 | 112 | private static IDisposable CreateSubscription2(string id, GraphQLHttpClient client) 113 | { 114 | #pragma warning disable 618 115 | var stream = client.CreateSubscriptionStream(new GraphQLRequest(@" 116 | subscription { 117 | contentAdded{ 118 | content 119 | from { 120 | displayName 121 | } 122 | } 123 | }" 124 | ) 125 | { Variables = new { id } }); 126 | #pragma warning restore 618 127 | 128 | return stream.Subscribe( 129 | response => Console.WriteLine($"{id}: new content from \"{response.Data.contentAdded.from.displayName.Value}\": {response.Data.contentAdded.content.Value}"), 130 | exception => Console.WriteLine($"{id}: content subscription stream failed: {exception}"), 131 | () => Console.WriteLine($"{id}: content subscription stream completed")); 132 | 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /SubscriptionIntegrationTest.ConsoleClient/SubscriptionIntegrationTest.ConsoleClient.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.0;net461 6 | 8.0 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /assets/logo.64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-dotnet/graphql-client/6236c9b2c6f3568f96a9f8e12aa6fb367321c068/assets/logo.64x64.png -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "isRoot": true, 3 | "tools": { 4 | "dotnet-format": { 5 | "version": "3.2.107702", 6 | "commands": [ 7 | "dotnet-format" 8 | ] 9 | }, 10 | "gitversion.tool": { 11 | "version": "5.12.0", 12 | "commands": [ 13 | "dotnet-gitversion" 14 | ] 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /examples/.editorconfig: -------------------------------------------------------------------------------- 1 | # Configure await 2 | configure_await_analysis_mode = disabled 3 | -------------------------------------------------------------------------------- /examples/GraphQL.Client.Example/GraphQL.Client.Example.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/GraphQL.Client.Example/PersonAndFilmsResponse.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Client.Example; 2 | 3 | public class PersonAndFilmsResponse 4 | { 5 | public PersonContent Person { get; set; } 6 | 7 | public class PersonContent 8 | { 9 | public string Name { get; set; } 10 | 11 | public FilmConnectionContent FilmConnection { get; set; } 12 | 13 | public class FilmConnectionContent 14 | { 15 | public List Films { get; set; } 16 | 17 | public class FilmContent 18 | { 19 | public string Title { get; set; } 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/GraphQL.Client.Example/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using GraphQL.Client.Http; 3 | using GraphQL.Client.Serializer.Newtonsoft; 4 | 5 | namespace GraphQL.Client.Example; 6 | 7 | public static class Program 8 | { 9 | public static async Task Main() 10 | { 11 | using var graphQLClient = new GraphQLHttpClient("https://swapi.apis.guru/", new NewtonsoftJsonSerializer()); 12 | 13 | var personAndFilmsRequest = new GraphQLRequest 14 | { 15 | Query = @" 16 | query PersonAndFilms($id: ID) { 17 | person(id: $id) { 18 | name 19 | filmConnection { 20 | films { 21 | title 22 | } 23 | } 24 | } 25 | }", 26 | OperationName = "PersonAndFilms", 27 | Variables = new 28 | { 29 | id = "cGVvcGxlOjE=" 30 | } 31 | }; 32 | 33 | var graphQLResponse = await graphQLClient.SendQueryAsync(personAndFilmsRequest); 34 | Console.WriteLine("raw response:"); 35 | Console.WriteLine(JsonSerializer.Serialize(graphQLResponse, new JsonSerializerOptions { WriteIndented = true })); 36 | 37 | Console.WriteLine(); 38 | Console.WriteLine($"Name: {graphQLResponse.Data.Person.Name}"); 39 | var films = string.Join(", ", graphQLResponse.Data.Person.FilmConnection.Films.Select(f => f.Title)); 40 | Console.WriteLine($"Films: {films}"); 41 | 42 | Console.WriteLine(); 43 | Console.WriteLine("Press any key to quit..."); 44 | Console.ReadKey(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/GraphQL.Client.Abstractions.Websocket/GraphQL.Client.Abstractions.Websocket.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Abstractions for the Websocket transport used in GraphQL.Client 5 | netstandard2.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/GraphQL.Client.Abstractions.Websocket/GraphQLWebSocketRequest.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Client.Abstractions.Websocket; 2 | 3 | /// 4 | /// A Subscription Request 5 | /// 6 | public class GraphQLWebSocketRequest : Dictionary, IEquatable 7 | { 8 | public const string ID_KEY = "id"; 9 | public const string TYPE_KEY = "type"; 10 | public const string PAYLOAD_KEY = "payload"; 11 | 12 | /// 13 | /// The Identifier of the request 14 | /// 15 | public string Id 16 | { 17 | get => TryGetValue(ID_KEY, out object value) ? (string)value : null; 18 | set => this[ID_KEY] = value; 19 | } 20 | 21 | /// 22 | /// The Type of the Request 23 | /// 24 | public string Type 25 | { 26 | get => TryGetValue(TYPE_KEY, out object value) ? (string)value : null; 27 | set => this[TYPE_KEY] = value; 28 | } 29 | 30 | /// 31 | /// The payload of the websocket request 32 | /// 33 | public object? Payload 34 | { 35 | get => TryGetValue(PAYLOAD_KEY, out object value) ? value : null; 36 | set => this[PAYLOAD_KEY] = value; 37 | } 38 | 39 | private readonly TaskCompletionSource _tcs = new TaskCompletionSource(); 40 | 41 | /// 42 | /// Task used to await the actual send operation and to convey potential exceptions 43 | /// 44 | /// 45 | public Task SendTask() => _tcs.Task; 46 | 47 | /// 48 | /// gets called when the send operation for this request has completed successfully 49 | /// 50 | public void SendCompleted() => _tcs.SetResult(true); 51 | 52 | /// 53 | /// gets called when an exception occurs during the send operation 54 | /// 55 | /// 56 | public void SendFailed(Exception e) => _tcs.SetException(e); 57 | 58 | /// 59 | /// gets called when the GraphQLHttpWebSocket has been disposed before the send operation for this request has started 60 | /// 61 | public void SendCanceled() => _tcs.SetCanceled(); 62 | 63 | /// 64 | public override bool Equals(object obj) => Equals(obj as GraphQLWebSocketRequest); 65 | 66 | /// 67 | public bool Equals(GraphQLWebSocketRequest other) 68 | { 69 | if (other == null) 70 | { 71 | return false; 72 | } 73 | if (ReferenceEquals(this, other)) 74 | { 75 | return true; 76 | } 77 | if (!Equals(Id, other.Id)) 78 | { 79 | return false; 80 | } 81 | if (!Equals(Type, other.Type)) 82 | { 83 | return false; 84 | } 85 | if (!Equals(Payload, other.Payload)) 86 | { 87 | return false; 88 | } 89 | return true; 90 | } 91 | 92 | /// 93 | public override int GetHashCode() 94 | { 95 | var hashCode = 9958074; 96 | hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Id); 97 | hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Type); 98 | hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Payload); 99 | return hashCode; 100 | } 101 | 102 | /// 103 | public static bool operator ==(GraphQLWebSocketRequest request1, GraphQLWebSocketRequest request2) => EqualityComparer.Default.Equals(request1, request2); 104 | 105 | /// 106 | public static bool operator !=(GraphQLWebSocketRequest request1, GraphQLWebSocketRequest request2) => !(request1 == request2); 107 | } 108 | -------------------------------------------------------------------------------- /src/GraphQL.Client.Abstractions.Websocket/GraphQLWebSocketResponse.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Client.Abstractions.Websocket; 2 | 3 | /// 4 | /// A Subscription Response 5 | /// 6 | public class GraphQLWebSocketResponse : IEquatable 7 | { 8 | /// 9 | /// The Identifier of the Response 10 | /// 11 | public string Id { get; set; } 12 | 13 | /// 14 | /// The Type of the Response 15 | /// 16 | public string Type { get; set; } 17 | 18 | /// 19 | public override bool Equals(object obj) => Equals(obj as GraphQLWebSocketResponse); 20 | 21 | /// 22 | public bool Equals(GraphQLWebSocketResponse other) 23 | { 24 | if (other == null) 25 | { 26 | return false; 27 | } 28 | 29 | if (ReferenceEquals(this, other)) 30 | { 31 | return true; 32 | } 33 | 34 | if (!Equals(Id, other.Id)) 35 | { 36 | return false; 37 | } 38 | 39 | if (!Equals(Type, other.Type)) 40 | { 41 | return false; 42 | } 43 | 44 | return true; 45 | } 46 | 47 | /// 48 | public override int GetHashCode() 49 | { 50 | var hashCode = 9958074; 51 | hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Id); 52 | hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Type); 53 | return hashCode; 54 | } 55 | 56 | /// 57 | public static bool operator ==(GraphQLWebSocketResponse response1, GraphQLWebSocketResponse response2) => 58 | EqualityComparer.Default.Equals(response1, response2); 59 | 60 | /// 61 | public static bool operator !=(GraphQLWebSocketResponse response1, GraphQLWebSocketResponse response2) => 62 | !(response1 == response2); 63 | } 64 | 65 | public class GraphQLWebSocketResponse : GraphQLWebSocketResponse, IEquatable> 66 | { 67 | public TPayload Payload { get; set; } 68 | 69 | public bool Equals(GraphQLWebSocketResponse? other) 70 | { 71 | if (other is null) 72 | return false; 73 | if (ReferenceEquals(this, other)) 74 | return true; 75 | return base.Equals(other) && Payload.Equals(other.Payload); 76 | } 77 | 78 | public override bool Equals(object? obj) 79 | { 80 | if (obj is null) 81 | return false; 82 | if (ReferenceEquals(this, obj)) 83 | return true; 84 | if (obj.GetType() != GetType()) 85 | return false; 86 | return Equals((GraphQLWebSocketResponse)obj); 87 | } 88 | 89 | public override int GetHashCode() 90 | { 91 | unchecked 92 | { 93 | return (base.GetHashCode() * 397) ^ Payload.GetHashCode(); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/GraphQL.Client.Abstractions.Websocket/GraphQLWebsocketConnectionState.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Client.Abstractions.Websocket; 2 | 3 | public enum GraphQLWebsocketConnectionState 4 | { 5 | Disconnected, 6 | 7 | Connecting, 8 | 9 | Connected 10 | } 11 | -------------------------------------------------------------------------------- /src/GraphQL.Client.Abstractions.Websocket/IGraphQLWebSocketClient.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Client.Abstractions.Websocket; 2 | 3 | public interface IGraphQLWebSocketClient : IGraphQLClient 4 | { 5 | /// 6 | /// The negotiated websocket sub-protocol. Will be while no websocket connection is established. 7 | /// 8 | string? WebSocketSubProtocol { get; } 9 | 10 | /// 11 | /// Publishes all exceptions which occur inside the websocket receive stream (i.e. for logging purposes) 12 | /// 13 | IObservable WebSocketReceiveErrors { get; } 14 | 15 | /// 16 | /// Publishes the websocket connection state 17 | /// 18 | IObservable WebsocketConnectionState { get; } 19 | 20 | /// 21 | /// Explicitly opens the websocket connection. Will be closed again on disposing the last subscription. 22 | /// 23 | Task InitializeWebsocketConnection(); 24 | 25 | /// 26 | /// Publishes the payload of all received pong messages (which may be ). Subscribing initiates the websocket connection.
27 | /// Ping/Pong is only supported when using the "graphql-transport-ws" websocket sub-protocol. 28 | ///
29 | /// the negotiated websocket sub-protocol does not support ping/pong 30 | IObservable PongStream { get; } 31 | 32 | /// 33 | /// Sends a ping to the server.
34 | /// Ping/Pong is only supported when using the "graphql-transport-ws" websocket sub-protocol. 35 | ///
36 | /// the negotiated websocket sub-protocol does not support ping/pong 37 | Task SendPingAsync(object? payload); 38 | 39 | /// 40 | /// Sends a pong to the server. This can be used for keep-alive scenarios (the client will automatically respond to pings received from the server).
41 | /// Ping/Pong is only supported when using the "graphql-transport-ws" websocket sub-protocol. 42 | ///
43 | /// the negotiated websocket sub-protocol does not support ping/pong 44 | Task SendPongAsync(object? payload); 45 | } 46 | -------------------------------------------------------------------------------- /src/GraphQL.Client.Abstractions.Websocket/IGraphQLWebsocketJsonSerializer.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Client.Abstractions.Websocket; 2 | 3 | /// 4 | /// The json serializer interface for the graphql-dotnet http client. 5 | /// Implementations should provide a parameterless constructor for convenient usage 6 | /// 7 | public interface IGraphQLWebsocketJsonSerializer : IGraphQLJsonSerializer 8 | { 9 | byte[] SerializeToBytes(GraphQLWebSocketRequest request); 10 | 11 | Task DeserializeToWebsocketResponseWrapperAsync(Stream stream); 12 | 13 | GraphQLWebSocketResponse DeserializeToWebsocketResponse(byte[] bytes); 14 | } 15 | -------------------------------------------------------------------------------- /src/GraphQL.Client.Abstractions.Websocket/WebsocketMessageWrapper.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace GraphQL.Client.Abstractions.Websocket; 4 | 5 | public class WebsocketMessageWrapper : GraphQLWebSocketResponse 6 | { 7 | 8 | [IgnoreDataMember] 9 | public byte[] MessageBytes { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /src/GraphQL.Client.Abstractions/GraphQL.Client.Abstractions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Abstractions for GraphQL.Client 5 | netstandard2.0;net6.0;net7.0;net8.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/GraphQL.Client.Abstractions/GraphQLClientExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace GraphQL.Client.Abstractions; 4 | 5 | public static class GraphQLClientExtensions 6 | { 7 | public static Task> SendQueryAsync(this IGraphQLClient client, 8 | [StringSyntax("GraphQL")] string query, object? variables = null, 9 | string? operationName = null, Func? defineResponseType = null, CancellationToken cancellationToken = default) 10 | { 11 | _ = defineResponseType; 12 | return client.SendQueryAsync(new GraphQLRequest(query, variables, operationName), 13 | cancellationToken: cancellationToken); 14 | } 15 | 16 | public static Task> SendQueryAsync(this IGraphQLClient client, 17 | GraphQLQuery query, object? variables = null, 18 | string? operationName = null, Func? defineResponseType = null, 19 | CancellationToken cancellationToken = default) 20 | => SendQueryAsync(client, query.Text, variables, operationName, defineResponseType, 21 | cancellationToken); 22 | 23 | public static Task> SendMutationAsync(this IGraphQLClient client, 24 | [StringSyntax("GraphQL")] string query, object? variables = null, 25 | string? operationName = null, Func? defineResponseType = null, CancellationToken cancellationToken = default) 26 | { 27 | _ = defineResponseType; 28 | return client.SendMutationAsync(new GraphQLRequest(query, variables, operationName), 29 | cancellationToken: cancellationToken); 30 | } 31 | 32 | public static Task> SendMutationAsync(this IGraphQLClient client, 33 | GraphQLQuery query, object? variables = null, string? operationName = null, Func? defineResponseType = null, 34 | CancellationToken cancellationToken = default) 35 | => SendMutationAsync(client, query.Text, variables, operationName, defineResponseType, 36 | cancellationToken); 37 | 38 | public static Task> SendQueryAsync(this IGraphQLClient client, 39 | GraphQLRequest request, Func defineResponseType, CancellationToken cancellationToken = default) 40 | { 41 | _ = defineResponseType; 42 | return client.SendQueryAsync(request, cancellationToken); 43 | } 44 | 45 | public static Task> SendMutationAsync(this IGraphQLClient client, 46 | GraphQLRequest request, Func defineResponseType, CancellationToken cancellationToken = default) 47 | { 48 | _ = defineResponseType; 49 | return client.SendMutationAsync(request, cancellationToken); 50 | } 51 | 52 | public static IObservable> CreateSubscriptionStream( 53 | this IGraphQLClient client, GraphQLRequest request, Func defineResponseType) 54 | { 55 | _ = defineResponseType; 56 | return client.CreateSubscriptionStream(request); 57 | } 58 | 59 | public static IObservable> CreateSubscriptionStream( 60 | this IGraphQLClient client, GraphQLRequest request, Func defineResponseType, Action exceptionHandler) 61 | { 62 | _ = defineResponseType; 63 | return client.CreateSubscriptionStream(request, exceptionHandler); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/GraphQL.Client.Abstractions/GraphQLJsonSerializerExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Client.Abstractions; 2 | 3 | public static class GraphQLJsonSerializerExtensions 4 | { 5 | public static TOptions New(this Action configure) => 6 | configure.AndReturn(Activator.CreateInstance()); 7 | 8 | public static TOptions AndReturn(this Action configure, TOptions options) 9 | { 10 | configure(options); 11 | return options; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/GraphQL.Client.Abstractions/IGraphQLClient.cs: -------------------------------------------------------------------------------- 1 | using System.Net.WebSockets; 2 | 3 | namespace GraphQL.Client.Abstractions; 4 | 5 | public interface IGraphQLClient 6 | { 7 | Task> SendQueryAsync(GraphQLRequest request, CancellationToken cancellationToken = default); 8 | 9 | Task> SendMutationAsync(GraphQLRequest request, CancellationToken cancellationToken = default); 10 | 11 | /// 12 | /// Creates a subscription to a GraphQL server. The connection is not established until the first actual subscription is made.
13 | /// All subscriptions made to this stream share the same hot observable.
14 | /// The stream must be recreated completely after an error has occurred within its logic (i.e. a ) 15 | ///
16 | /// the GraphQL request for this subscription 17 | /// an observable stream for the specified subscription 18 | IObservable> CreateSubscriptionStream(GraphQLRequest request); 19 | 20 | /// 21 | /// Creates a subscription to a GraphQL server. The connection is not established until the first actual subscription is made.
22 | /// All subscriptions made to this stream share the same hot observable.
23 | /// All s are passed to the to be handled externally.
24 | /// If the completes normally, the subscription is recreated with a new connection attempt.
25 | /// Any exception thrown by will cause the sequence to fail. 26 | ///
27 | /// the GraphQL request for this subscription 28 | /// an external handler for all s occurring within the sequence 29 | /// an observable stream for the specified subscription 30 | IObservable> CreateSubscriptionStream(GraphQLRequest request, Action exceptionHandler); 31 | } 32 | -------------------------------------------------------------------------------- /src/GraphQL.Client.Abstractions/IGraphQLJsonSerializer.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Client.Abstractions; 2 | 3 | public interface IGraphQLJsonSerializer 4 | { 5 | string SerializeToString(GraphQLRequest request); 6 | 7 | Task> DeserializeFromUtf8StreamAsync(Stream stream, CancellationToken cancellationToken); 8 | } 9 | -------------------------------------------------------------------------------- /src/GraphQL.Client.Abstractions/Utilities/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Client.Abstractions.Utilities; 2 | 3 | /// 4 | /// Copied from https://github.com/jquense/StringUtils 5 | /// 6 | public static class StringExtensions 7 | { 8 | public static string StripIndent(this string str) => GraphQL.Client.Abstractions.Utilities.StringUtils.StripIndent(str); 9 | 10 | public static IEnumerable ToWords(this string str) => GraphQL.Client.Abstractions.Utilities.StringUtils.ToWords(str); 11 | 12 | public static string ToUpperFirst(this string str) => GraphQL.Client.Abstractions.Utilities.StringUtils.ToUpperFirst(str); 13 | 14 | public static string ToLowerFirst(this string str) => GraphQL.Client.Abstractions.Utilities.StringUtils.ToLowerFirst(str); 15 | 16 | public static string Capitalize(this string str) => GraphQL.Client.Abstractions.Utilities.StringUtils.Capitalize(str); 17 | 18 | public static string ToCamelCase(this string str) => GraphQL.Client.Abstractions.Utilities.StringUtils.ToCamelCase(str); 19 | 20 | public static string ToConstantCase(this string str) => GraphQL.Client.Abstractions.Utilities.StringUtils.ToConstantCase(str); 21 | 22 | public static string ToUpperCase(this string str) => GraphQL.Client.Abstractions.Utilities.StringUtils.ToUpperCase(str); 23 | 24 | public static string ToLowerCase(this string str) => GraphQL.Client.Abstractions.Utilities.StringUtils.ToLowerCase(str); 25 | 26 | 27 | public static string ToPascalCase(this string str) => GraphQL.Client.Abstractions.Utilities.StringUtils.ToPascalCase(str); 28 | 29 | 30 | public static string ToKebabCase(this string str) => GraphQL.Client.Abstractions.Utilities.StringUtils.ToKebabCase(str); 31 | 32 | 33 | public static string ToSnakeCase(this string str) => GraphQL.Client.Abstractions.Utilities.StringUtils.ToSnakeCase(str); 34 | } 35 | -------------------------------------------------------------------------------- /src/GraphQL.Client.LocalExecution/GraphQL.Client.LocalExecution.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | A GraphQL Client which executes the queries directly on a provided GraphQL schema using graphql-dotnet 5 | netstandard2.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/GraphQL.Client.LocalExecution/GraphQLLocalExecutionClient.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Linq; 2 | using System.Reactive.Threading.Tasks; 3 | using GraphQL.Client.Abstractions; 4 | using GraphQL.Types; 5 | 6 | namespace GraphQL.Client.LocalExecution; 7 | 8 | public static class GraphQLLocalExecutionClient 9 | { 10 | public static GraphQLLocalExecutionClient New(TSchema schema, IGraphQLJsonSerializer clientSerializer, IGraphQLTextSerializer serverSerializer) 11 | where TSchema : ISchema 12 | => new(schema, new DocumentExecuter(), clientSerializer, serverSerializer); 13 | } 14 | 15 | public class GraphQLLocalExecutionClient : IGraphQLClient where TSchema : ISchema 16 | { 17 | public TSchema Schema { get; } 18 | 19 | public IGraphQLJsonSerializer Serializer { get; } 20 | 21 | private readonly IDocumentExecuter _documentExecuter; 22 | private readonly IGraphQLTextSerializer _documentSerializer; 23 | 24 | public GraphQLLocalExecutionClient(TSchema schema, IDocumentExecuter documentExecuter, IGraphQLJsonSerializer serializer, IGraphQLTextSerializer documentSerializer) 25 | { 26 | Schema = schema ?? throw new ArgumentNullException(nameof(schema), "no schema configured"); 27 | _documentExecuter = documentExecuter; 28 | Serializer = serializer ?? throw new ArgumentNullException(nameof(serializer), "please configure the JSON serializer you want to use"); 29 | _documentSerializer = documentSerializer; 30 | 31 | if (!Schema.Initialized) 32 | Schema.Initialize(); 33 | } 34 | 35 | public void Dispose() { } 36 | 37 | public Task> SendQueryAsync(GraphQLRequest request, CancellationToken cancellationToken = default) 38 | => ExecuteQueryAsync(request, cancellationToken); 39 | 40 | public Task> SendMutationAsync(GraphQLRequest request, CancellationToken cancellationToken = default) 41 | => ExecuteQueryAsync(request, cancellationToken); 42 | 43 | public IObservable> CreateSubscriptionStream(GraphQLRequest request) => 44 | Observable.Defer(() => ExecuteSubscriptionAsync(request).ToObservable()) 45 | .Concat() 46 | .Publish() 47 | .RefCount(); 48 | 49 | public IObservable> CreateSubscriptionStream(GraphQLRequest request, 50 | Action exceptionHandler) 51 | => CreateSubscriptionStream(request); 52 | 53 | #region Private Methods 54 | 55 | private async Task> ExecuteQueryAsync(GraphQLRequest request, CancellationToken cancellationToken) 56 | { 57 | var executionResult = await ExecuteAsync(request, cancellationToken).ConfigureAwait(false); 58 | return await ExecutionResultToGraphQLResponseAsync(executionResult, cancellationToken).ConfigureAwait(false); 59 | } 60 | 61 | private async Task>> ExecuteSubscriptionAsync(GraphQLRequest request, CancellationToken cancellationToken = default) 62 | { 63 | var result = await ExecuteAsync(request, cancellationToken).ConfigureAwait(false); 64 | var stream = result.Streams?.Values.SingleOrDefault(); 65 | 66 | return stream == null 67 | ? Observable.Throw>(new InvalidOperationException("the GraphQL execution did not return an observable")) 68 | : stream.SelectMany(executionResult => Observable.FromAsync(token => ExecutionResultToGraphQLResponseAsync(executionResult, token))); 69 | } 70 | 71 | private async Task ExecuteAsync(GraphQLRequest clientRequest, CancellationToken cancellationToken = default) 72 | { 73 | var serverRequest = _documentSerializer.Deserialize(Serializer.SerializeToString(clientRequest)); 74 | 75 | var result = await _documentExecuter.ExecuteAsync(options => 76 | { 77 | options.Schema = Schema; 78 | options.OperationName = serverRequest?.OperationName; 79 | options.Query = serverRequest?.Query; 80 | options.Variables = serverRequest?.Variables; 81 | options.Extensions = serverRequest?.Extensions; 82 | options.CancellationToken = cancellationToken; 83 | }).ConfigureAwait(false); 84 | 85 | return result; 86 | } 87 | 88 | private async Task> ExecutionResultToGraphQLResponseAsync(ExecutionResult executionResult, CancellationToken cancellationToken = default) 89 | { 90 | using var stream = new MemoryStream(); 91 | await _documentSerializer.WriteAsync(stream, executionResult, cancellationToken).ConfigureAwait(false); 92 | stream.Seek(0, SeekOrigin.Begin); 93 | return await Serializer.DeserializeFromUtf8StreamAsync(stream, cancellationToken).ConfigureAwait(false); 94 | } 95 | 96 | #endregion 97 | } 98 | -------------------------------------------------------------------------------- /src/GraphQL.Client.LocalExecution/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Client.Abstractions; 2 | using GraphQL.DI; 3 | using GraphQL.MicrosoftDI; 4 | using GraphQL.Types; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace GraphQL.Client.LocalExecution; 8 | 9 | public static class ServiceCollectionExtensions 10 | { 11 | public static IGraphQLBuilder AddGraphQLLocalExecutionClient(this IServiceCollection services) where TSchema : ISchema 12 | { 13 | services.AddSingleton>(); 14 | services.AddSingleton(p => p.GetRequiredService>()); 15 | return new GraphQLBuilder(services, null); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/GraphQL.Client.Serializer.Newtonsoft/ConstantCaseEnumConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using GraphQL.Client.Abstractions.Utilities; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Converters; 5 | 6 | namespace GraphQL.Client.Serializer.Newtonsoft; 7 | 8 | public class ConstantCaseEnumConverter : StringEnumConverter 9 | { 10 | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 11 | { 12 | if (value == null) 13 | { 14 | writer.WriteNull(); 15 | } 16 | else 17 | { 18 | var enumString = ((Enum)value).ToString("G"); 19 | var memberName = value.GetType() 20 | .GetMember(enumString, BindingFlags.DeclaredOnly | BindingFlags.Static | BindingFlags.Public) 21 | .FirstOrDefault()?.Name; 22 | if (string.IsNullOrEmpty(memberName)) 23 | { 24 | if (!AllowIntegerValues) 25 | throw new JsonSerializationException($"Integer value {value} is not allowed."); 26 | writer.WriteValue(value); 27 | } 28 | else 29 | { 30 | writer.WriteValue(memberName.ToConstantCase()); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/GraphQL.Client.Serializer.Newtonsoft/GraphQL.Client.Serializer.Newtonsoft.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | A serializer implementation for GraphQL.Client using Newtonsoft.Json as underlying JSON library 5 | netstandard2.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/GraphQL.Client.Serializer.Newtonsoft/MapConverter.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | 4 | namespace GraphQL.Client.Serializer.Newtonsoft; 5 | 6 | public class MapConverter : JsonConverter 7 | { 8 | public override void WriteJson(JsonWriter writer, Map value, JsonSerializer serializer) => 9 | throw new NotImplementedException( 10 | "This converter currently is only intended to be used to read a JSON object into a strongly-typed representation."); 11 | 12 | public override Map? ReadJson(JsonReader reader, Type objectType, Map existingValue, bool hasExistingValue, JsonSerializer serializer) 13 | { 14 | var rootToken = JToken.ReadFrom(reader); 15 | return rootToken.Type switch 16 | { 17 | JTokenType.Object => (Map)ReadDictionary(rootToken, new Map()), 18 | JTokenType.Null => null, 19 | _ => throw new ArgumentException("This converter can only parse when the root element is a JSON Object.") 20 | }; 21 | } 22 | 23 | private object? ReadToken(JToken? token) => 24 | token switch 25 | { 26 | JObject jObject => ReadDictionary(jObject, new Dictionary()), 27 | JArray jArray => ReadArray(jArray).ToList(), 28 | JValue jValue => jValue.Value, 29 | JConstructor _ => throw new ArgumentOutOfRangeException(nameof(token.Type), 30 | "cannot deserialize a JSON constructor"), 31 | JProperty _ => throw new ArgumentOutOfRangeException(nameof(token.Type), 32 | "cannot deserialize a JSON property"), 33 | JContainer _ => throw new ArgumentOutOfRangeException(nameof(token.Type), 34 | "cannot deserialize a JSON comment"), 35 | _ => throw new ArgumentOutOfRangeException(nameof(token.Type)) 36 | }; 37 | 38 | private Dictionary ReadDictionary(JToken element, Dictionary to) 39 | { 40 | foreach (var property in ((JObject)element).Properties()) 41 | { 42 | if (IsUnsupportedJTokenType(property.Value.Type)) 43 | continue; 44 | to[property.Name] = ReadToken(property.Value); 45 | } 46 | return to; 47 | } 48 | 49 | private IEnumerable ReadArray(JArray element) 50 | { 51 | foreach (var item in element) 52 | { 53 | if (IsUnsupportedJTokenType(item.Type)) 54 | continue; 55 | yield return ReadToken(item); 56 | } 57 | } 58 | 59 | private bool IsUnsupportedJTokenType(JTokenType type) => type == JTokenType.Constructor || type == JTokenType.Property || type == JTokenType.Comment; 60 | } 61 | -------------------------------------------------------------------------------- /src/GraphQL.Client.Serializer.Newtonsoft/NewtonsoftJsonSerializer.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using GraphQL.Client.Abstractions; 3 | using GraphQL.Client.Abstractions.Websocket; 4 | using Newtonsoft.Json; 5 | using Newtonsoft.Json.Serialization; 6 | 7 | namespace GraphQL.Client.Serializer.Newtonsoft; 8 | 9 | public class NewtonsoftJsonSerializer : IGraphQLWebsocketJsonSerializer 10 | { 11 | public static JsonSerializerSettings DefaultJsonSerializerSettings => new() 12 | { 13 | ContractResolver = new CamelCasePropertyNamesContractResolver { IgnoreIsSpecifiedMembers = true }, 14 | MissingMemberHandling = MissingMemberHandling.Ignore, 15 | Converters = { new ConstantCaseEnumConverter() } 16 | }; 17 | 18 | public JsonSerializerSettings JsonSerializerSettings { get; } 19 | 20 | public NewtonsoftJsonSerializer() : this(DefaultJsonSerializerSettings) { } 21 | 22 | public NewtonsoftJsonSerializer(Action configure) : this(configure.AndReturn(DefaultJsonSerializerSettings)) { } 23 | 24 | public NewtonsoftJsonSerializer(JsonSerializerSettings jsonSerializerSettings) 25 | { 26 | JsonSerializerSettings = jsonSerializerSettings; 27 | ConfigureMandatorySerializerOptions(); 28 | } 29 | 30 | // deserialize extensions to Dictionary 31 | private void ConfigureMandatorySerializerOptions() => JsonSerializerSettings.Converters.Insert(0, new MapConverter()); 32 | 33 | public string SerializeToString(GraphQLRequest request) => JsonConvert.SerializeObject(request, JsonSerializerSettings); 34 | 35 | public byte[] SerializeToBytes(GraphQLWebSocketRequest request) 36 | { 37 | string json = JsonConvert.SerializeObject(request, JsonSerializerSettings); 38 | return Encoding.UTF8.GetBytes(json); 39 | } 40 | 41 | public Task DeserializeToWebsocketResponseWrapperAsync(Stream stream) => DeserializeFromUtf8Stream(stream); 42 | 43 | public GraphQLWebSocketResponse DeserializeToWebsocketResponse(byte[] bytes) => 44 | JsonConvert.DeserializeObject>(Encoding.UTF8.GetString(bytes), 45 | JsonSerializerSettings); 46 | 47 | public Task> DeserializeFromUtf8StreamAsync(Stream stream, CancellationToken cancellationToken) => DeserializeFromUtf8Stream>(stream); 48 | 49 | private Task DeserializeFromUtf8Stream(Stream stream) 50 | { 51 | using var sr = new StreamReader(stream); 52 | using JsonReader reader = new JsonTextReader(sr); 53 | var serializer = JsonSerializer.Create(JsonSerializerSettings); 54 | return Task.FromResult(serializer.Deserialize(reader)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/GraphQL.Client.Serializer.SystemTextJson/ConstantCaseJsonNamingPolicy.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using GraphQL.Client.Abstractions.Utilities; 3 | 4 | namespace GraphQL.Client.Serializer.SystemTextJson; 5 | 6 | public class ConstantCaseJsonNamingPolicy : JsonNamingPolicy 7 | { 8 | public override string ConvertName(string name) => name.ToConstantCase(); 9 | } 10 | -------------------------------------------------------------------------------- /src/GraphQL.Client.Serializer.SystemTextJson/ConverterHelperExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.Numerics; 3 | using System.Text; 4 | using System.Text.Json; 5 | 6 | namespace GraphQL.Client.Serializer.SystemTextJson; 7 | 8 | public static class ConverterHelperExtensions 9 | { 10 | public static object ReadNumber(this ref Utf8JsonReader reader) 11 | { 12 | if (reader.TryGetInt32(out int i)) 13 | return i; 14 | else if (reader.TryGetInt64(out long l)) 15 | return l; 16 | else if (reader.TryGetDouble(out double d)) 17 | return reader.TryGetBigInteger(out var bi) && bi != new BigInteger(d) 18 | ? bi 19 | : (object)d; 20 | else if (reader.TryGetDecimal(out decimal dd)) 21 | return reader.TryGetBigInteger(out var bi) && bi != new BigInteger(dd) 22 | ? bi 23 | : (object)dd; 24 | 25 | throw new NotImplementedException($"Unexpected Number value. Raw text was: {reader.GetRawString()}"); 26 | } 27 | 28 | public static bool TryGetBigInteger(this ref Utf8JsonReader reader, out BigInteger bi) => BigInteger.TryParse(reader.GetRawString(), out bi); 29 | 30 | public static string GetRawString(this ref Utf8JsonReader reader) 31 | { 32 | var byteArray = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan.ToArray(); 33 | return Encoding.UTF8.GetString(byteArray); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/GraphQL.Client.Serializer.SystemTextJson/ErrorPathConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace GraphQL.Client.Serializer.SystemTextJson; 5 | 6 | public class ErrorPathConverter : JsonConverter 7 | { 8 | 9 | public override ErrorPath Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => 10 | new(ReadArray(ref reader)); 11 | 12 | public override void Write(Utf8JsonWriter writer, ErrorPath value, JsonSerializerOptions options) 13 | => throw new NotImplementedException( 14 | "This converter currently is only intended to be used to read a JSON object into a strongly-typed representation."); 15 | 16 | private static IEnumerable ReadArray(ref Utf8JsonReader reader) 17 | { 18 | if (reader.TokenType != JsonTokenType.StartArray) 19 | { 20 | throw new JsonException("This converter can only parse when the root element is a JSON Array."); 21 | } 22 | 23 | var array = new List(); 24 | 25 | while (reader.Read()) 26 | { 27 | if (reader.TokenType == JsonTokenType.EndArray) 28 | break; 29 | 30 | array.Add(ReadValue(ref reader)); 31 | } 32 | 33 | return array; 34 | } 35 | 36 | private static object? ReadValue(ref Utf8JsonReader reader) 37 | => reader.TokenType switch 38 | { 39 | JsonTokenType.None => null, 40 | JsonTokenType.String => reader.GetString(), 41 | JsonTokenType.Number => reader.ReadNumber(), 42 | JsonTokenType.True => true, 43 | JsonTokenType.False => false, 44 | JsonTokenType.Null => null, 45 | _ => throw new InvalidOperationException($"Unexpected token type: {reader.TokenType}") 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/GraphQL.Client.Serializer.SystemTextJson/GraphQL.Client.Serializer.SystemTextJson.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | A serializer implementation for GraphQL.Client using System.Text.Json as underlying JSON library 5 | netstandard2.0;netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/GraphQL.Client.Serializer.SystemTextJson/JsonSerializerOptionsExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | namespace GraphQL.Client.Serializer.SystemTextJson; 4 | 5 | public static class JsonSerializerOptionsExtensions 6 | { 7 | public static JsonSerializerOptions SetupImmutableConverter(this JsonSerializerOptions options) 8 | { 9 | options.Converters.Add(new ImmutableConverter()); 10 | return options; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/GraphQL.Client.Serializer.SystemTextJson/MapConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace GraphQL.Client.Serializer.SystemTextJson; 5 | 6 | /// 7 | /// A custom JsonConverter for reading the extension fields of and . 8 | /// 9 | /// 10 | /// Taken and modified from GraphQL.SystemTextJson.ObjectDictionaryConverter (GraphQL.NET) 11 | /// 12 | public class MapConverter : JsonConverter 13 | { 14 | public override Map Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => ReadDictionary(ref reader, new Map()); 15 | 16 | public override void Write(Utf8JsonWriter writer, Map value, JsonSerializerOptions options) 17 | => throw new NotImplementedException( 18 | "This converter currently is only intended to be used to read a JSON object into a strongly-typed representation."); 19 | 20 | private static TDictionary ReadDictionary(ref Utf8JsonReader reader, TDictionary result) 21 | where TDictionary : Dictionary 22 | { 23 | if (reader.TokenType != JsonTokenType.StartObject) 24 | throw new JsonException(); 25 | 26 | while (reader.Read()) 27 | { 28 | if (reader.TokenType == JsonTokenType.EndObject) 29 | break; 30 | 31 | if (reader.TokenType != JsonTokenType.PropertyName) 32 | throw new JsonException(); 33 | 34 | string key = reader.GetString(); 35 | 36 | // move to property value 37 | if (!reader.Read()) 38 | throw new JsonException(); 39 | 40 | result.Add(key, ReadValue(ref reader)); 41 | } 42 | 43 | return result; 44 | } 45 | 46 | private static List ReadArray(ref Utf8JsonReader reader) 47 | { 48 | if (reader.TokenType != JsonTokenType.StartArray) 49 | throw new JsonException(); 50 | 51 | var result = new List(); 52 | 53 | while (reader.Read()) 54 | { 55 | if (reader.TokenType == JsonTokenType.EndArray) 56 | break; 57 | 58 | result.Add(ReadValue(ref reader)); 59 | } 60 | 61 | return result; 62 | } 63 | 64 | private static object? ReadValue(ref Utf8JsonReader reader) 65 | => reader.TokenType switch 66 | { 67 | JsonTokenType.StartArray => ReadArray(ref reader).ToList(), 68 | JsonTokenType.StartObject => ReadDictionary(ref reader, new Dictionary()), 69 | JsonTokenType.Number => reader.ReadNumber(), 70 | JsonTokenType.True => true, 71 | JsonTokenType.False => false, 72 | JsonTokenType.String => reader.GetString(), 73 | JsonTokenType.Null => null, 74 | JsonTokenType.None => null, 75 | _ => throw new InvalidOperationException($"Unexpected value kind: {reader.TokenType}") 76 | }; 77 | } 78 | -------------------------------------------------------------------------------- /src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | using GraphQL.Client.Abstractions; 4 | using GraphQL.Client.Abstractions.Websocket; 5 | 6 | namespace GraphQL.Client.Serializer.SystemTextJson; 7 | 8 | public class SystemTextJsonSerializer : IGraphQLWebsocketJsonSerializer 9 | { 10 | public static JsonSerializerOptions DefaultJsonSerializerOptions => new JsonSerializerOptions 11 | { 12 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 13 | Converters = { new JsonStringEnumConverter(new ConstantCaseJsonNamingPolicy(), false) } 14 | }.SetupImmutableConverter(); 15 | 16 | public JsonSerializerOptions Options { get; } 17 | 18 | public SystemTextJsonSerializer() : this(DefaultJsonSerializerOptions) { } 19 | 20 | public SystemTextJsonSerializer(Action configure) : this(configure.AndReturn(DefaultJsonSerializerOptions)) { } 21 | 22 | public SystemTextJsonSerializer(JsonSerializerOptions options) 23 | { 24 | Options = options; 25 | ConfigureMandatorySerializerOptions(); 26 | } 27 | 28 | private void ConfigureMandatorySerializerOptions() 29 | { 30 | // deserialize extensions to Dictionary 31 | Options.Converters.Insert(0, new ErrorPathConverter()); 32 | Options.Converters.Insert(0, new MapConverter()); 33 | // allow the JSON field "data" to match the property "Data" even without JsonNamingPolicy.CamelCase 34 | Options.PropertyNameCaseInsensitive = true; 35 | } 36 | 37 | public string SerializeToString(GraphQLRequest request) => JsonSerializer.Serialize(request, Options); 38 | 39 | public Task> DeserializeFromUtf8StreamAsync(Stream stream, CancellationToken cancellationToken) => JsonSerializer.DeserializeAsync>(stream, Options, cancellationToken).AsTask(); 40 | 41 | public byte[] SerializeToBytes(GraphQLWebSocketRequest request) => JsonSerializer.SerializeToUtf8Bytes(request, Options); 42 | 43 | public Task DeserializeToWebsocketResponseWrapperAsync(Stream stream) => JsonSerializer.DeserializeAsync(stream, Options).AsTask(); 44 | 45 | public GraphQLWebSocketResponse DeserializeToWebsocketResponse(byte[] bytes) => 46 | JsonSerializer.Deserialize>(new ReadOnlySpan(bytes), 47 | Options); 48 | } 49 | -------------------------------------------------------------------------------- /src/GraphQL.Client/GraphQL.Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;net461;net6.0;net7.0;net8.0 5 | GraphQL.Client.Http 6 | 7 | 8 | 9 | NETSTANDARD 10 | 11 | 12 | 13 | NETFRAMEWORK 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/GraphQL.Client/GraphQLHttpClientExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Net.WebSockets; 2 | using GraphQL.Client.Abstractions; 3 | 4 | namespace GraphQL.Client.Http; 5 | 6 | public static class GraphQLHttpClientExtensions 7 | { 8 | /// 9 | /// Creates a subscription to a GraphQL server. The connection is not established until the first actual subscription is made.
10 | /// All subscriptions made to this stream share the same hot observable.
11 | /// All s are passed to the to be handled externally.
12 | /// If the completes normally, the subscription is recreated with a new connection attempt.
13 | /// Other s or any exception thrown by will cause the sequence to fail. 14 | ///
15 | /// the GraphQL client 16 | /// the GraphQL request for this subscription 17 | /// an external handler for all s occurring within the sequence 18 | /// an observable stream for the specified subscription 19 | public static IObservable> CreateSubscriptionStream(this IGraphQLClient client, 20 | GraphQLRequest request, Action webSocketExceptionHandler) => 21 | client.CreateSubscriptionStream(request, e => 22 | { 23 | if (e is WebSocketException webSocketException) 24 | webSocketExceptionHandler(webSocketException); 25 | else 26 | throw e; 27 | }); 28 | 29 | /// 30 | public static IObservable> CreateSubscriptionStream( 31 | this IGraphQLClient client, GraphQLRequest request, Func defineResponseType, Action webSocketExceptionHandler) 32 | { 33 | _ = defineResponseType; 34 | return client.CreateSubscriptionStream(request, webSocketExceptionHandler); 35 | } 36 | 37 | /// 38 | public static IObservable> CreateSubscriptionStream( 39 | this IGraphQLClient client, GraphQLRequest request, Func defineResponseType) 40 | { 41 | _ = defineResponseType; 42 | return client.CreateSubscriptionStream(request); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/GraphQL.Client/GraphQLHttpRequest.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable IDE0005 2 | // see https://learn.microsoft.com/en-us/dotnet/core/compatibility/sdk/8.0/implicit-global-using-netfx 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Net.Http; 5 | #pragma warning restore IDE0005 6 | using System.Net.Http.Headers; 7 | using System.Text; 8 | using GraphQL.Client.Abstractions; 9 | 10 | namespace GraphQL.Client.Http; 11 | 12 | public class GraphQLHttpRequest : GraphQLRequest 13 | { 14 | public GraphQLHttpRequest() 15 | { 16 | } 17 | 18 | public GraphQLHttpRequest([StringSyntax("GraphQL")] string query, object? variables = null, string? operationName = null, Dictionary? extensions = null) 19 | : base(query, variables, operationName, extensions) 20 | { 21 | } 22 | public GraphQLHttpRequest(GraphQLQuery query, object? variables = null, string? operationName = null, Dictionary? extensions = null) 23 | : base(query, variables, operationName, extensions) 24 | { 25 | } 26 | 27 | public GraphQLHttpRequest(GraphQLRequest other) 28 | : base(other) 29 | { 30 | } 31 | 32 | /// 33 | /// Creates a from this . 34 | /// Used by to convert GraphQL requests when sending them as regular HTTP requests. 35 | /// 36 | /// the passed from 37 | /// the passed from 38 | /// 39 | public virtual HttpRequestMessage ToHttpRequestMessage(GraphQLHttpClientOptions options, IGraphQLJsonSerializer serializer) 40 | { 41 | var message = new HttpRequestMessage(HttpMethod.Post, options.EndPoint) 42 | { 43 | Content = new StringContent(serializer.SerializeToString(this), Encoding.UTF8, options.MediaType) 44 | }; 45 | message.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/graphql-response+json")); 46 | message.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 47 | message.Headers.AcceptCharset.Add(new StringWithQualityHeaderValue("utf-8")); 48 | 49 | // Explicitly setting content header to avoid issues with some GrahQL servers 50 | message.Content.Headers.ContentType = new MediaTypeHeaderValue(options.MediaType); 51 | 52 | if (options.DefaultUserAgentRequestHeader != null) 53 | message.Headers.UserAgent.Add(options.DefaultUserAgentRequestHeader); 54 | 55 | return message; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/GraphQL.Client/GraphQLHttpRequestException.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Http.Headers; 3 | 4 | namespace GraphQL.Client.Http; 5 | 6 | /// 7 | /// An exception thrown on unexpected 8 | /// 9 | public class GraphQLHttpRequestException : Exception 10 | { 11 | /// 12 | /// The returned status code 13 | /// 14 | public HttpStatusCode StatusCode { get; } 15 | 16 | /// 17 | /// the returned response headers 18 | /// 19 | public HttpResponseHeaders ResponseHeaders { get; } 20 | 21 | /// 22 | /// the returned content 23 | /// 24 | public string? Content { get; } 25 | 26 | /// 27 | /// Creates a new instance of 28 | /// 29 | /// 30 | /// 31 | /// 32 | public GraphQLHttpRequestException(HttpStatusCode statusCode, HttpResponseHeaders responseHeaders, string? content) : base($"The HTTP request failed with status code {statusCode}") 33 | { 34 | StatusCode = statusCode; 35 | ResponseHeaders = responseHeaders; 36 | Content = content; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/GraphQL.Client/GraphQLHttpResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Http.Headers; 3 | 4 | namespace GraphQL.Client.Http; 5 | 6 | public class GraphQLHttpResponse : GraphQLResponse, IGraphQLHttpResponse 7 | { 8 | public GraphQLHttpResponse(GraphQLResponse response, HttpResponseHeaders responseHeaders, HttpStatusCode statusCode) 9 | { 10 | Data = response.Data; 11 | Errors = response.Errors; 12 | Extensions = response.Extensions; 13 | ResponseHeaders = responseHeaders; 14 | StatusCode = statusCode; 15 | } 16 | 17 | public HttpResponseHeaders ResponseHeaders { get; set; } 18 | 19 | public HttpStatusCode StatusCode { get; set; } 20 | } 21 | 22 | public interface IGraphQLHttpResponse : IGraphQLResponse 23 | { 24 | HttpResponseHeaders ResponseHeaders { get; set; } 25 | 26 | HttpStatusCode StatusCode { get; set; } 27 | } 28 | 29 | public static class GraphQLResponseExtensions 30 | { 31 | public static GraphQLHttpResponse ToGraphQLHttpResponse(this GraphQLResponse response, HttpResponseHeaders responseHeaders, HttpStatusCode statusCode) => new(response, responseHeaders, statusCode); 32 | 33 | /// 34 | /// Casts to . Throws if the cast fails. 35 | /// 36 | /// 37 | /// 38 | /// is not a 39 | /// 40 | public static GraphQLHttpResponse AsGraphQLHttpResponse(this GraphQLResponse response) => (GraphQLHttpResponse)response; 41 | } 42 | -------------------------------------------------------------------------------- /src/GraphQL.Client/GraphQLSubscriptionException.cs: -------------------------------------------------------------------------------- 1 | #if !NET8_0_OR_GREATER 2 | using System.Runtime.Serialization; 3 | #endif 4 | 5 | namespace GraphQL.Client.Http; 6 | 7 | [Serializable] 8 | public class GraphQLSubscriptionException : Exception 9 | { 10 | public GraphQLSubscriptionException() 11 | { 12 | } 13 | 14 | public GraphQLSubscriptionException(object error) : base(error.ToString()) 15 | { 16 | } 17 | 18 | #if !NET8_0_OR_GREATER 19 | protected GraphQLSubscriptionException( 20 | SerializationInfo info, 21 | StreamingContext context) : base(info, context) 22 | { 23 | } 24 | #endif 25 | } 26 | -------------------------------------------------------------------------------- /src/GraphQL.Client/UriExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Client.Http; 2 | 3 | public static class UriExtensions 4 | { 5 | /// 6 | /// Returns true if equals "wss" or "ws" 7 | /// 8 | /// 9 | /// 10 | public static bool HasWebSocketScheme(this Uri? uri) => 11 | uri is not null && 12 | (uri.Scheme.Equals("wss", StringComparison.OrdinalIgnoreCase) || uri.Scheme.Equals("ws", StringComparison.OrdinalIgnoreCase)); 13 | 14 | /// 15 | /// Infers the websocket uri from . 16 | /// 17 | /// 18 | /// 19 | public static Uri GetWebSocketUri(this Uri uri) 20 | { 21 | if (uri is null) 22 | throw new ArgumentNullException(nameof(uri)); 23 | 24 | if (uri.HasWebSocketScheme()) 25 | return uri; 26 | 27 | string webSocketScheme; 28 | 29 | if (uri.Scheme == Uri.UriSchemeHttps) 30 | webSocketScheme = "wss"; 31 | else if (uri.Scheme == Uri.UriSchemeHttp) 32 | webSocketScheme = "ws"; 33 | else 34 | throw new NotSupportedException($"cannot infer websocket uri from uri scheme {uri.Scheme}"); 35 | 36 | return new UriBuilder(uri) { Scheme = webSocketScheme }.Uri; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/GraphQL.Client/Websocket/GraphQLWebsocketConnectionException.cs: -------------------------------------------------------------------------------- 1 | #if !NET8_0_OR_GREATER 2 | using System.Runtime.Serialization; 3 | #endif 4 | 5 | namespace GraphQL.Client.Http.Websocket; 6 | 7 | [Serializable] 8 | public class GraphQLWebsocketConnectionException : Exception 9 | { 10 | public GraphQLWebsocketConnectionException() 11 | { 12 | } 13 | 14 | public GraphQLWebsocketConnectionException(string message) : base(message) 15 | { 16 | } 17 | 18 | public GraphQLWebsocketConnectionException(string message, Exception innerException) : base(message, innerException) 19 | { 20 | } 21 | 22 | #if !NET8_0_OR_GREATER 23 | protected GraphQLWebsocketConnectionException(SerializationInfo info, StreamingContext context) : base(info, context) 24 | { 25 | } 26 | #endif 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/GraphQL.Client/Websocket/IWebsocketProtocolHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Disposables; 2 | using GraphQL.Client.Abstractions.Websocket; 3 | 4 | namespace GraphQL.Client.Http.Websocket; 5 | 6 | public interface IWebsocketProtocolHandler 7 | { 8 | string WebsocketProtocol { get; } 9 | 10 | IObservable> CreateSubscriptionObservable(GraphQLRequest request); 11 | 12 | IObservable> CreateGraphQLRequestObservable(GraphQLRequest request); 13 | 14 | IObservable CreatePongObservable(); 15 | 16 | Task InitializeConnectionAsync(IObservable incomingMessages, CompositeDisposable closeConnectionDisposable); 17 | 18 | Task SendCloseConnectionRequestAsync(); 19 | 20 | Task SendPingAsync(object? payload); 21 | 22 | Task SendPongAsync(object? payload); 23 | } 24 | -------------------------------------------------------------------------------- /src/GraphQL.Client/Websocket/WebSocketProtocols.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace GraphQL.Client.Http.Websocket; 4 | public static class WebSocketProtocols 5 | { 6 | public const string AUTO_NEGOTIATE = null; 7 | 8 | //The WebSocket sub-protocol used for the [GraphQL over WebSocket Protocol](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md). 9 | public const string GRAPHQL_TRANSPORT_WS = "graphql-transport-ws"; 10 | 11 | //The deprecated subprotocol used by [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws). 12 | public const string GRAPHQL_WS = "graphql-ws"; 13 | 14 | public static IEnumerable GetSupportedWebSocketProtocols() => 15 | typeof(WebSocketProtocols) 16 | .GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy) 17 | .Where(info => (info.IsLiteral || info.IsInitOnly) && info.FieldType == typeof(string)) 18 | .Select(f => f.IsLiteral ? (string)f.GetRawConstantValue() : (string)f.GetValue(null)) 19 | .Where(s => s is not null); 20 | } 21 | -------------------------------------------------------------------------------- /src/GraphQL.Primitives/ErrorPath.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL; 2 | 3 | public class ErrorPath : List 4 | { 5 | public ErrorPath() 6 | { 7 | } 8 | 9 | public ErrorPath(IEnumerable collection) : base(collection) 10 | { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/GraphQL.Primitives/GraphQL.Primitives.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | GraphQL basic types 5 | GraphQL 6 | netstandard2.0;net6.0;net7.0;net8.0 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/GraphQL.Primitives/GraphQLError.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace GraphQL; 4 | 5 | /// 6 | /// Represents a GraphQL Error of a GraphQL Query 7 | /// 8 | public class GraphQLError : IEquatable 9 | { 10 | /// 11 | /// The locations of the error 12 | /// 13 | [DataMember(Name = "locations")] 14 | public GraphQLLocation[]? Locations { get; set; } 15 | 16 | /// 17 | /// The message of the error 18 | /// 19 | [DataMember(Name = "message")] 20 | public string Message { get; set; } 21 | 22 | /// 23 | /// The Path of the error 24 | /// 25 | [DataMember(Name = "path")] 26 | public ErrorPath? Path { get; set; } 27 | 28 | /// 29 | /// The extensions of the error 30 | /// 31 | [DataMember(Name = "extensions")] 32 | public Map? Extensions { get; set; } 33 | 34 | /// 35 | /// Returns a value that indicates whether this instance is equal to a specified object 36 | /// 37 | /// The object to compare with this instance 38 | /// true if obj is an instance of and equals the value of the instance; otherwise, false 39 | public override bool Equals(object? obj) => Equals(obj as GraphQLError); 40 | 41 | /// 42 | /// Returns a value that indicates whether this instance is equal to a specified object 43 | /// 44 | /// The object to compare with this instance 45 | /// true if obj is an instance of and equals the value of the instance; otherwise, false 46 | public bool Equals(GraphQLError? other) 47 | { 48 | if (other == null) 49 | { return false; } 50 | if (ReferenceEquals(this, other)) 51 | { return true; } 52 | { 53 | if (Locations != null && other.Locations != null) 54 | { 55 | if (!Locations.SequenceEqual(other.Locations)) 56 | { return false; } 57 | } 58 | else if (Locations != null && other.Locations == null) 59 | { return false; } 60 | else if (Locations == null && other.Locations != null) 61 | { return false; } 62 | } 63 | if (!EqualityComparer.Default.Equals(Message, other.Message)) 64 | { return false; } 65 | { 66 | if (Path != null && other.Path != null) 67 | { 68 | if (!Path.SequenceEqual(other.Path)) 69 | { return false; } 70 | } 71 | else if (Path != null && other.Path == null) 72 | { return false; } 73 | else if (Path == null && other.Path != null) 74 | { return false; } 75 | } 76 | return true; 77 | } 78 | 79 | /// 80 | /// 81 | /// 82 | public override int GetHashCode() 83 | { 84 | var hashCode = 0; 85 | if (Locations != null) 86 | { 87 | hashCode ^= EqualityComparer.Default.GetHashCode(Locations); 88 | } 89 | hashCode ^= EqualityComparer.Default.GetHashCode(Message); 90 | if (Path != null) 91 | { 92 | hashCode ^= EqualityComparer.Default.GetHashCode(Path); 93 | } 94 | return hashCode; 95 | } 96 | 97 | /// 98 | /// Tests whether two specified instances are equivalent 99 | /// 100 | /// The instance that is to the left of the equality operator 101 | /// The instance that is to the right of the equality operator 102 | /// true if left and right are equal; otherwise, false 103 | public static bool operator ==(GraphQLError? left, GraphQLError? right) => 104 | EqualityComparer.Default.Equals(left, right); 105 | 106 | /// 107 | /// Tests whether two specified instances are not equal 108 | /// 109 | /// The instance that is to the left of the not equal operator 110 | /// The instance that is to the right of the not equal operator 111 | /// true if left and right are unequal; otherwise, false 112 | public static bool operator !=(GraphQLError? left, GraphQLError? right) => 113 | !EqualityComparer.Default.Equals(left, right); 114 | } 115 | -------------------------------------------------------------------------------- /src/GraphQL.Primitives/GraphQLLocation.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL; 2 | 3 | /// 4 | /// Represents a GraphQL Location of a GraphQL Query 5 | /// 6 | public sealed class GraphQLLocation : IEquatable 7 | { 8 | /// 9 | /// The Column 10 | /// 11 | public uint Column { get; set; } 12 | 13 | /// 14 | /// The Line 15 | /// 16 | public uint Line { get; set; } 17 | 18 | /// 19 | /// Returns a value that indicates whether this instance is equal to a specified object 20 | /// 21 | /// The object to compare with this instance 22 | /// true if obj is an instance of and equals the value of the instance; otherwise, false 23 | public override bool Equals(object obj) => Equals(obj as GraphQLLocation); 24 | 25 | /// 26 | /// Returns a value that indicates whether this instance is equal to a specified object 27 | /// 28 | /// The object to compare with this instance 29 | /// true if obj is an instance of and equals the value of the instance; otherwise, false 30 | public bool Equals(GraphQLLocation? other) 31 | { 32 | if (other == null) 33 | { return false; } 34 | if (ReferenceEquals(this, other)) 35 | { return true; } 36 | return EqualityComparer.Default.Equals(Column, other.Column) && 37 | EqualityComparer.Default.Equals(Line, other.Line); 38 | } 39 | 40 | /// 41 | /// 42 | /// 43 | public override int GetHashCode() => 44 | Column.GetHashCode() ^ Line.GetHashCode(); 45 | 46 | /// 47 | /// Tests whether two specified instances are equivalent 48 | /// 49 | /// The instance that is to the left of the equality operator 50 | /// The instance that is to the right of the equality operator 51 | /// true if left and right are equal; otherwise, false 52 | public static bool operator ==(GraphQLLocation? left, GraphQLLocation? right) => 53 | EqualityComparer.Default.Equals(left, right); 54 | 55 | /// 56 | /// Tests whether two specified instances are not equal 57 | /// 58 | /// The instance that is to the left of the not equal operator 59 | /// The instance that is to the right of the not equal operator 60 | /// true if left and right are unequal; otherwise, false 61 | public static bool operator !=(GraphQLLocation? left, GraphQLLocation? right) => 62 | !EqualityComparer.Default.Equals(left, right); 63 | } 64 | -------------------------------------------------------------------------------- /src/GraphQL.Primitives/GraphQLQuery.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | namespace GraphQL; 3 | 4 | /// 5 | /// Value object representing a GraphQL query string and storing the corresponding APQ hash.
6 | /// Use this to hold query strings you want to use more than once. 7 | ///
8 | public class GraphQLQuery : IEquatable 9 | { 10 | /// 11 | /// The actual query string 12 | /// 13 | public string Text { get; } 14 | 15 | /// 16 | /// The SHA256 hash used for the automatic persisted queries feature (APQ) 17 | /// 18 | public string Sha256Hash { get; } 19 | 20 | public GraphQLQuery([StringSyntax("GraphQL")] string text) 21 | { 22 | Text = text; 23 | Sha256Hash = Hash.Compute(Text); 24 | } 25 | 26 | public static implicit operator string(GraphQLQuery query) 27 | => query.Text; 28 | 29 | public bool Equals(GraphQLQuery other) => Sha256Hash == other.Sha256Hash; 30 | 31 | public override bool Equals(object? obj) => obj is GraphQLQuery other && Equals(other); 32 | 33 | public override int GetHashCode() => Sha256Hash.GetHashCode(); 34 | } 35 | -------------------------------------------------------------------------------- /src/GraphQL.Primitives/GraphQLRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace GraphQL; 4 | 5 | /// 6 | /// A GraphQL request 7 | /// 8 | public class GraphQLRequest : Dictionary, IEquatable 9 | { 10 | public const string OPERATION_NAME_KEY = "operationName"; 11 | public const string QUERY_KEY = "query"; 12 | public const string VARIABLES_KEY = "variables"; 13 | public const string EXTENSIONS_KEY = "extensions"; 14 | public const string EXTENSIONS_PERSISTED_QUERY_KEY = "persistedQuery"; 15 | public const int APQ_SUPPORTED_VERSION = 1; 16 | 17 | private string? _sha265Hash; 18 | 19 | /// 20 | /// The query string 21 | /// 22 | [StringSyntax("GraphQL")] 23 | public string? Query 24 | { 25 | get => TryGetValue(QUERY_KEY, out object value) ? (string)value : null; 26 | set 27 | { 28 | this[QUERY_KEY] = value; 29 | // if the query string gets overwritten, reset the hash value 30 | _sha265Hash = null; 31 | } 32 | } 33 | 34 | /// 35 | /// The operation to execute 36 | /// 37 | public string? OperationName 38 | { 39 | get => TryGetValue(OPERATION_NAME_KEY, out object value) ? (string)value : null; 40 | set => this[OPERATION_NAME_KEY] = value; 41 | } 42 | 43 | /// 44 | /// Represents the request variables 45 | /// 46 | public object? Variables 47 | { 48 | get => TryGetValue(VARIABLES_KEY, out object value) ? value : null; 49 | set => this[VARIABLES_KEY] = value; 50 | } 51 | 52 | /// 53 | /// Represents the request extensions 54 | /// 55 | public Dictionary? Extensions 56 | { 57 | get => TryGetValue(EXTENSIONS_KEY, out object value) && value is Dictionary d ? d : null; 58 | set => this[EXTENSIONS_KEY] = value; 59 | } 60 | 61 | public GraphQLRequest() { } 62 | 63 | public GraphQLRequest([StringSyntax("GraphQL")] string query, object? variables = null, string? operationName = null, Dictionary? extensions = null) 64 | { 65 | Query = query; 66 | Variables = variables; 67 | OperationName = operationName; 68 | Extensions = extensions; 69 | } 70 | 71 | public GraphQLRequest(GraphQLQuery query, object? variables = null, string? operationName = null, 72 | Dictionary? extensions = null) 73 | : this(query.Text, variables, operationName, extensions) 74 | { 75 | _sha265Hash = query.Sha256Hash; 76 | } 77 | 78 | public GraphQLRequest(GraphQLRequest other) : base(other) { } 79 | 80 | public void GeneratePersistedQueryExtension() 81 | { 82 | if (Query is null) 83 | throw new InvalidOperationException($"{nameof(Query)} is null"); 84 | 85 | Extensions ??= new(); 86 | Extensions[EXTENSIONS_PERSISTED_QUERY_KEY] = new Dictionary 87 | { 88 | ["version"] = APQ_SUPPORTED_VERSION, 89 | ["sha256Hash"] = _sha265Hash ??= Hash.Compute(Query), 90 | }; 91 | } 92 | 93 | /// 94 | /// Returns a value that indicates whether this instance is equal to a specified object 95 | /// 96 | /// The object to compare with this instance 97 | /// true if obj is an instance of and equals the value of the instance; otherwise, false 98 | public override bool Equals(object? obj) 99 | { 100 | if (obj is null) 101 | return false; 102 | if (ReferenceEquals(this, obj)) 103 | return true; 104 | if (obj.GetType() != GetType()) 105 | return false; 106 | return Equals((GraphQLRequest)obj); 107 | } 108 | 109 | /// 110 | /// Returns a value that indicates whether this instance is equal to a specified object 111 | /// 112 | /// The object to compare with this instance 113 | /// true if obj is an instance of and equals the value of the instance; otherwise, false 114 | public virtual bool Equals(GraphQLRequest? other) 115 | { 116 | if (other is null) 117 | return false; 118 | if (ReferenceEquals(this, other)) 119 | return true; 120 | return Count == other.Count && !this.Except(other).Any(); 121 | } 122 | 123 | /// 124 | /// 125 | /// 126 | public override int GetHashCode() => (Query, OperationName, Variables, Extensions).GetHashCode(); 127 | 128 | /// 129 | /// Tests whether two specified instances are equivalent 130 | /// 131 | /// The instance that is to the left of the equality operator 132 | /// The instance that is to the right of the equality operator 133 | /// true if left and right are equal; otherwise, false 134 | public static bool operator ==(GraphQLRequest? left, GraphQLRequest? right) => EqualityComparer.Default.Equals(left, right); 135 | 136 | /// 137 | /// Tests whether two specified instances are not equal 138 | /// 139 | /// The instance that is to the left of the not equal operator 140 | /// The instance that is to the right of the not equal operator 141 | /// true if left and right are unequal; otherwise, false 142 | public static bool operator !=(GraphQLRequest? left, GraphQLRequest? right) => !(left == right); 143 | } 144 | -------------------------------------------------------------------------------- /src/GraphQL.Primitives/GraphQLResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace GraphQL; 4 | 5 | public class GraphQLResponse : IGraphQLResponse, IEquatable?> 6 | { 7 | [DataMember(Name = "data")] 8 | public T Data { get; set; } 9 | object IGraphQLResponse.Data => Data; 10 | 11 | [DataMember(Name = "errors")] 12 | public GraphQLError[]? Errors { get; set; } 13 | 14 | [DataMember(Name = "extensions")] 15 | public Map? Extensions { get; set; } 16 | 17 | public override bool Equals(object? obj) => Equals(obj as GraphQLResponse); 18 | 19 | public bool Equals(GraphQLResponse? other) 20 | { 21 | if (other == null) 22 | { return false; } 23 | if (ReferenceEquals(this, other)) 24 | { return true; } 25 | if (!EqualityComparer.Default.Equals(Data, other.Data)) 26 | { return false; } 27 | 28 | if (Errors != null && other.Errors != null) 29 | { 30 | if (!Enumerable.SequenceEqual(Errors, other.Errors)) 31 | { return false; } 32 | } 33 | else if (Errors != null && other.Errors == null) 34 | { return false; } 35 | else if (Errors == null && other.Errors != null) 36 | { return false; } 37 | 38 | if (Extensions != null && other.Extensions != null) 39 | { 40 | if (!Enumerable.SequenceEqual(Extensions, other.Extensions)) 41 | { return false; } 42 | } 43 | else if (Extensions != null && other.Extensions == null) 44 | { return false; } 45 | else if (Extensions == null && other.Extensions != null) 46 | { return false; } 47 | 48 | return true; 49 | } 50 | 51 | public override int GetHashCode() 52 | { 53 | unchecked 54 | { 55 | var hashCode = EqualityComparer.Default.GetHashCode(Data); 56 | { 57 | if (Errors != null) 58 | { 59 | foreach (var element in Errors) 60 | { 61 | hashCode = (hashCode * 397) ^ EqualityComparer.Default.GetHashCode(element); 62 | } 63 | } 64 | else 65 | { 66 | hashCode = (hashCode * 397) ^ 0; 67 | } 68 | 69 | if (Extensions != null) 70 | { 71 | foreach (var element in Extensions) 72 | { 73 | hashCode = (hashCode * 397) ^ EqualityComparer>.Default.GetHashCode(element); 74 | } 75 | } 76 | else 77 | { 78 | hashCode = (hashCode * 397) ^ 0; 79 | } 80 | } 81 | return hashCode; 82 | } 83 | } 84 | 85 | public static bool operator ==(GraphQLResponse? response1, GraphQLResponse? response2) => EqualityComparer?>.Default.Equals(response1, response2); 86 | 87 | public static bool operator !=(GraphQLResponse? response1, GraphQLResponse? response2) => !(response1 == response2); 88 | } 89 | -------------------------------------------------------------------------------- /src/GraphQL.Primitives/Hash.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.Diagnostics; 3 | using System.Security.Cryptography; 4 | using System.Text; 5 | 6 | namespace GraphQL; 7 | 8 | internal static class Hash 9 | { 10 | private static SHA256? _sha256; 11 | 12 | internal static string Compute(string query) 13 | { 14 | int expected = Encoding.UTF8.GetByteCount(query); 15 | byte[]? inputBytes = ArrayPool.Shared.Rent(expected); 16 | int written = Encoding.UTF8.GetBytes(query, 0, query.Length, inputBytes, 0); 17 | Debug.Assert(written == expected, (string)$"Encoding.UTF8.GetBytes returned unexpected bytes: {written} instead of {expected}"); 18 | 19 | var shaShared = Interlocked.Exchange(ref _sha256, null) ?? SHA256.Create(); 20 | 21 | #if NET5_0_OR_GREATER 22 | Span bytes = stackalloc byte[32]; 23 | if (!shaShared.TryComputeHash(inputBytes.AsSpan().Slice(0, written), bytes, out int bytesWritten)) // bytesWritten ignored since it is always 32 24 | throw new InvalidOperationException("Too small buffer for hash"); 25 | #else 26 | byte[] bytes = shaShared.ComputeHash(inputBytes, 0, written); 27 | #endif 28 | 29 | ArrayPool.Shared.Return(inputBytes); 30 | Interlocked.CompareExchange(ref _sha256, shaShared, null); 31 | 32 | #if NET5_0_OR_GREATER 33 | return Convert.ToHexString(bytes); 34 | #else 35 | var builder = new StringBuilder(bytes.Length * 2); 36 | foreach (byte item in bytes) 37 | { 38 | builder.Append(item.ToString("x2")); 39 | } 40 | 41 | return builder.ToString(); 42 | #endif 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/GraphQL.Primitives/IGraphQLResponse.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL; 2 | 3 | public interface IGraphQLResponse 4 | { 5 | object Data { get; } 6 | 7 | GraphQLError[]? Errors { get; set; } 8 | 9 | Map? Extensions { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /src/GraphQL.Primitives/Map.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL; 2 | 3 | /// 4 | /// A type equivalent to a javascript map. Create a custom json converter for this class to customize your serializers behaviour 5 | /// 6 | public class Map : Dictionary { } 7 | -------------------------------------------------------------------------------- /src/GraphQL.Primitives/StringSyntaxAttribute.cs: -------------------------------------------------------------------------------- 1 | #if !NET7_0_OR_GREATER 2 | 3 | // ReSharper disable InconsistentNaming 4 | // ReSharper disable once CheckNamespace 5 | namespace System.Diagnostics.CodeAnalysis; 6 | 7 | /// Stub version of the StringSyntaxAttribute, which was introduced in .NET 7 8 | public sealed class StringSyntaxAttribute : Attribute 9 | { 10 | /// Initializes the with the identifier of the syntax used. 11 | /// The syntax identifier. 12 | public StringSyntaxAttribute(string syntax) 13 | { 14 | Syntax = syntax; 15 | Arguments = Array.Empty(); 16 | } 17 | 18 | /// Initializes the with the identifier of the syntax used. 19 | /// The syntax identifier. 20 | /// Optional arguments associated with the specific syntax employed. 21 | public StringSyntaxAttribute(string syntax, params object?[] arguments) 22 | { 23 | Syntax = syntax; 24 | Arguments = arguments; 25 | } 26 | 27 | /// Gets the identifier of the syntax used. 28 | public string Syntax { get; } 29 | 30 | /// Optional arguments associated with the specific syntax employed. 31 | public object?[] Arguments { get; } 32 | 33 | /// The syntax identifier for strings containing composite formats for string formatting. 34 | #pragma warning disable IDE1006 35 | public const string CompositeFormat = nameof(CompositeFormat); 36 | 37 | /// The syntax identifier for strings containing date format specifiers. 38 | public const string DateOnlyFormat = nameof(DateOnlyFormat); 39 | 40 | /// The syntax identifier for strings containing date and time format specifiers. 41 | public const string DateTimeFormat = nameof(DateTimeFormat); 42 | 43 | /// The syntax identifier for strings containing format specifiers. 44 | public const string EnumFormat = nameof(EnumFormat); 45 | 46 | /// The syntax identifier for strings containing format specifiers. 47 | public const string GuidFormat = nameof(GuidFormat); 48 | 49 | /// The syntax identifier for strings containing JavaScript Object Notation (JSON). 50 | public const string Json = nameof(Json); 51 | 52 | /// The syntax identifier for strings containing numeric format specifiers. 53 | public const string NumericFormat = nameof(NumericFormat); 54 | 55 | /// The syntax identifier for strings containing regular expressions. 56 | public const string Regex = nameof(Regex); 57 | 58 | /// The syntax identifier for strings containing time format specifiers. 59 | public const string TimeOnlyFormat = nameof(TimeOnlyFormat); 60 | 61 | /// The syntax identifier for strings containing format specifiers. 62 | public const string TimeSpanFormat = nameof(TimeSpanFormat); 63 | 64 | /// The syntax identifier for strings containing URIs. 65 | public const string Uri = nameof(Uri); 66 | 67 | /// The syntax identifier for strings containing XML. 68 | public const string Xml = nameof(Xml); 69 | #pragma warning restore IDE1006 70 | } 71 | 72 | #endif 73 | -------------------------------------------------------------------------------- /tests/.editorconfig: -------------------------------------------------------------------------------- 1 | # Configure await 2 | configure_await_analysis_mode = disabled 3 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Serializer.Tests/BaseSerializeNoCamelCaseTest.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using FluentAssertions; 3 | using GraphQL.Client.Abstractions; 4 | using GraphQL.Client.Abstractions.Websocket; 5 | using GraphQL.Client.LocalExecution; 6 | using GraphQL.Client.Serializer.Tests.TestData; 7 | using GraphQL.Client.Tests.Common; 8 | using GraphQL.Client.Tests.Common.Helpers; 9 | using Xunit; 10 | 11 | namespace GraphQL.Client.Serializer.Tests; 12 | 13 | public abstract class BaseSerializeNoCamelCaseTest 14 | { 15 | public IGraphQLWebsocketJsonSerializer ClientSerializer { get; } 16 | 17 | public IGraphQLTextSerializer ServerSerializer { get; } 18 | 19 | public IGraphQLClient ChatClient { get; } 20 | 21 | public IGraphQLClient StarWarsClient { get; } 22 | 23 | protected BaseSerializeNoCamelCaseTest(IGraphQLWebsocketJsonSerializer clientSerializer, IGraphQLTextSerializer serverSerializer) 24 | { 25 | ClientSerializer = clientSerializer; 26 | ServerSerializer = serverSerializer; 27 | ChatClient = GraphQLLocalExecutionClient.New(Common.GetChatSchema(), clientSerializer, serverSerializer); 28 | StarWarsClient = GraphQLLocalExecutionClient.New(Common.GetStarWarsSchema(), clientSerializer, serverSerializer); 29 | } 30 | 31 | [Theory] 32 | [ClassData(typeof(SerializeToStringTestData))] 33 | public void SerializeToStringTest(string expectedJson, GraphQLRequest request) 34 | { 35 | var json = ClientSerializer.SerializeToString(request).RemoveWhitespace(); 36 | json.Should().Be(expectedJson.RemoveWhitespace()); 37 | } 38 | 39 | [Theory] 40 | [ClassData(typeof(SerializeToBytesTestData))] 41 | public void SerializeToBytesTest(string expectedJson, GraphQLWebSocketRequest request) 42 | { 43 | var json = Encoding.UTF8.GetString(ClientSerializer.SerializeToBytes(request)).RemoveWhitespace(); 44 | json.Should().Be(expectedJson.RemoveWhitespace()); 45 | } 46 | 47 | [Fact] 48 | public async void WorksWithoutCamelCaseNamingStrategy() 49 | { 50 | const string message = "some random testing message"; 51 | var graphQLRequest = new GraphQLRequest( 52 | @"mutation($input: MessageInputType){ 53 | addMessage(message: $input){ 54 | content 55 | } 56 | }", 57 | new 58 | { 59 | input = new 60 | { 61 | fromId = "2", 62 | content = message, 63 | sentAt = DateTime.Now 64 | } 65 | }); 66 | var response = await ChatClient.SendMutationAsync(graphQLRequest, () => new { addMessage = new { content = "" } }); 67 | 68 | Assert.Equal(message, response.Data.addMessage.content); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Serializer.Tests/ConsistencyTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using FluentAssertions.Execution; 3 | using GraphQL.Client.Serializer.Newtonsoft; 4 | using GraphQL.Client.Serializer.SystemTextJson; 5 | using Newtonsoft.Json; 6 | using Xunit; 7 | 8 | namespace GraphQL.Client.Serializer.Tests; 9 | 10 | public class ConsistencyTests 11 | { 12 | [Theory] 13 | [InlineData(@"{ 14 | ""array"": [ 15 | ""some stuff"", 16 | ""something else"" 17 | ], 18 | ""string"": ""this is a string"", 19 | ""boolean"": true, 20 | ""number"": 1234.567, 21 | ""nested object"": { 22 | ""prop1"": false 23 | }, 24 | ""arrayOfObjects"": [ 25 | {""number"": 1234.567}, 26 | {""number"": 567.8} 27 | ] 28 | }")] 29 | [InlineData("null")] 30 | public void MapConvertersShouldBehaveConsistent(string json) 31 | { 32 | //const string json = @"{ 33 | // ""array"": [ 34 | // ""some stuff"", 35 | // ""something else"" 36 | // ], 37 | // ""string"": ""this is a string"", 38 | // ""boolean"": true, 39 | // ""number"": 1234.567, 40 | // ""nested object"": { 41 | // ""prop1"": false 42 | // }, 43 | // ""arrayOfObjects"": [ 44 | // {""number"": 1234.567}, 45 | // {""number"": 567.8} 46 | // ] 47 | // }"; 48 | 49 | var newtonsoftSerializer = new NewtonsoftJsonSerializer(); 50 | var systemTextJsonSerializer = new SystemTextJsonSerializer(); 51 | 52 | var newtonsoftMap = JsonConvert.DeserializeObject(json, newtonsoftSerializer.JsonSerializerSettings); 53 | var systemTextJsonMap = System.Text.Json.JsonSerializer.Deserialize(json, systemTextJsonSerializer.Options); 54 | 55 | 56 | using (new AssertionScope()) 57 | { 58 | CompareMaps(newtonsoftMap, systemTextJsonMap); 59 | } 60 | 61 | newtonsoftMap.Should().BeEquivalentTo(systemTextJsonMap, options => options 62 | .RespectingRuntimeTypes()); 63 | } 64 | 65 | /// 66 | /// Regression test for https://github.com/graphql-dotnet/graphql-client/issues/601 67 | /// 68 | [Fact] 69 | public void MapConvertersShouldBeAbleToDeserializeNullValues() 70 | { 71 | var newtonsoftSerializer = new NewtonsoftJsonSerializer(); 72 | var systemTextJsonSerializer = new SystemTextJsonSerializer(); 73 | string json = "null"; 74 | 75 | JsonConvert.DeserializeObject(json, newtonsoftSerializer.JsonSerializerSettings).Should().BeNull(); 76 | System.Text.Json.JsonSerializer.Deserialize(json, systemTextJsonSerializer.Options).Should().BeNull(); 77 | } 78 | 79 | private void CompareMaps(Dictionary? first, Dictionary? second) 80 | { 81 | if (first is null) 82 | second.Should().BeNull(); 83 | else 84 | foreach (var keyValuePair in first) 85 | { 86 | second.Should().ContainKey(keyValuePair.Key); 87 | second[keyValuePair.Key].Should().BeOfType(keyValuePair.Value.GetType()); 88 | if (keyValuePair.Value is Dictionary map) 89 | CompareMaps(map, (Dictionary)second[keyValuePair.Key]); 90 | else 91 | keyValuePair.Value.Should().BeEquivalentTo(second[keyValuePair.Key]); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Serializer.Tests/DefaultValidationTest.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Http.Headers; 3 | using FluentAssertions; 4 | using GraphQL.Client.Http; 5 | using Xunit; 6 | 7 | namespace GraphQL.Client.Serializer.Tests; 8 | 9 | public class DefaultValidationTest 10 | { 11 | [Theory] 12 | [InlineData(HttpStatusCode.OK, "application/json", true)] 13 | [InlineData(HttpStatusCode.OK, "application/graphql-response+json", true)] 14 | [InlineData(HttpStatusCode.BadRequest, "application/json", true)] 15 | [InlineData(HttpStatusCode.BadRequest, "text/html", false)] 16 | [InlineData(HttpStatusCode.OK, "text/html", false)] 17 | [InlineData(HttpStatusCode.Forbidden, "text/html", false)] 18 | [InlineData(HttpStatusCode.Forbidden, "application/json", false)] 19 | public void IsValidResponse_OkJson_True(HttpStatusCode statusCode, string mediaType, bool expectedResult) 20 | { 21 | var response = new HttpResponseMessage(statusCode); 22 | response.Content.Headers.ContentType = new MediaTypeHeaderValue(mediaType); 23 | 24 | bool isValid = new GraphQLHttpClientOptions().IsValidResponseToDeserialize(response); 25 | 26 | isValid.Should().Be(expectedResult); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Serializer.Tests/GraphQL.Client.Serializer.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | net8 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | all 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | 27 | 28 | 29 | 30 | all 31 | runtime; build; native; contentfiles; analyzers; buildtransitive 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Serializer.Tests/NewtonsoftSerializerTest.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Client.Serializer.Newtonsoft; 2 | using GraphQL.Execution; 3 | using Newtonsoft.Json; 4 | 5 | namespace GraphQL.Client.Serializer.Tests; 6 | 7 | public class NewtonsoftSerializerTest : BaseSerializerTest 8 | { 9 | public NewtonsoftSerializerTest() 10 | : base( 11 | new NewtonsoftJsonSerializer(), 12 | new NewtonsoftJson.GraphQLSerializer(new ErrorInfoProvider(opt => opt.ExposeData = true))) 13 | { 14 | } 15 | } 16 | 17 | public class NewtonsoftSerializeNoCamelCaseTest : BaseSerializeNoCamelCaseTest 18 | { 19 | public NewtonsoftSerializeNoCamelCaseTest() 20 | : base( 21 | new NewtonsoftJsonSerializer(new JsonSerializerSettings { Converters = { new ConstantCaseEnumConverter() } }), 22 | new NewtonsoftJson.GraphQLSerializer(new ErrorInfoProvider(opt => opt.ExposeData = true))) 23 | { 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Serializer.Tests/SystemTextJsonSerializerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | using GraphQL.Client.Serializer.SystemTextJson; 4 | using GraphQL.Execution; 5 | 6 | namespace GraphQL.Client.Serializer.Tests; 7 | 8 | public class SystemTextJsonSerializerTests : BaseSerializerTest 9 | { 10 | public SystemTextJsonSerializerTests() 11 | : base( 12 | new SystemTextJsonSerializer(), 13 | new GraphQL.SystemTextJson.GraphQLSerializer(new ErrorInfoProvider(opt => opt.ExposeData = true))) 14 | { 15 | } 16 | } 17 | 18 | public class SystemTextJsonSerializeNoCamelCaseTest : BaseSerializeNoCamelCaseTest 19 | { 20 | public SystemTextJsonSerializeNoCamelCaseTest() 21 | : base( 22 | new SystemTextJsonSerializer(new JsonSerializerOptions { Converters = { new JsonStringEnumConverter(new ConstantCaseJsonNamingPolicy(), false) } }.SetupImmutableConverter()), 23 | new GraphQL.SystemTextJson.GraphQLSerializer(new ErrorInfoProvider(opt => opt.ExposeData = true))) 24 | { 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Serializer.Tests/TestData/DeserializeResponseTestData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace GraphQL.Client.Serializer.Tests.TestData; 4 | 5 | public class DeserializeResponseTestData : IEnumerable 6 | { 7 | public IEnumerator GetEnumerator() 8 | { 9 | // object array structure: 10 | // [0]: input json 11 | // [1]: expected deserialized response 12 | 13 | yield return new object[] { 14 | "{\"errors\":[{\"message\":\"Throttled\",\"extensions\":{\"code\":\"THROTTLED\",\"documentation\":\"https://help.shopify.com/api/graphql-admin-api/graphql-admin-api-rate-limits\"}}],\"extensions\":{\"cost\":{\"requestedQueryCost\":992,\"actualQueryCost\":null,\"throttleStatus\":{\"maximumAvailable\":1000,\"currentlyAvailable\":632,\"restoreRate\":50}}}}", 15 | new GraphQLResponse { 16 | Data = null, 17 | Errors = new[] { 18 | new GraphQLError { 19 | Message = "Throttled", 20 | Extensions = new Map { 21 | {"code", "THROTTLED" }, 22 | {"documentation", "https://help.shopify.com/api/graphql-admin-api/graphql-admin-api-rate-limits" } 23 | } 24 | } 25 | }, 26 | Extensions = new Map { 27 | {"cost", new Dictionary { 28 | {"requestedQueryCost", 992}, 29 | {"actualQueryCost", null}, 30 | {"throttleStatus", new Dictionary { 31 | {"maximumAvailable", 1000}, 32 | {"currentlyAvailable", 632}, 33 | {"restoreRate", 50} 34 | }} 35 | }} 36 | } 37 | } 38 | }; 39 | 40 | yield return new object[] 41 | { 42 | @"{ 43 | ""errors"": [ 44 | { 45 | ""message"": ""Name for character with ID 1002 could not be fetched."", 46 | ""locations"": [ 47 | { 48 | ""line"": 6, 49 | ""column"": 7 50 | } 51 | ], 52 | ""path"": [ 53 | ""hero"", 54 | ""heroFriends"", 55 | 1, 56 | ""name"" 57 | ] 58 | } 59 | ], 60 | ""data"": { 61 | ""hero"": { 62 | ""name"": ""R2-D2"", 63 | ""heroFriends"": [ 64 | { 65 | ""id"": ""1000"", 66 | ""name"": ""Luke Skywalker"" 67 | }, 68 | { 69 | ""id"": ""1002"", 70 | ""name"": null 71 | }, 72 | { 73 | ""id"": ""1003"", 74 | ""name"": ""Leia Organa"" 75 | } 76 | ] 77 | } 78 | } 79 | }", 80 | NewAnonymouslyTypedGraphQLResponse(new 81 | { 82 | hero = new 83 | { 84 | name = "R2-D2", 85 | heroFriends = new List 86 | { 87 | new Friend {Id = "1000", Name = "Luke Skywalker"}, 88 | new Friend {Id = "1002", Name = null}, 89 | new Friend {Id = "1003", Name = "Leia Organa"} 90 | } 91 | } 92 | }, 93 | new[] { 94 | new GraphQLError { 95 | Message = "Name for character with ID 1002 could not be fetched.", 96 | Locations = new [] { new GraphQLLocation{Line = 6, Column = 7 }}, 97 | Path = new ErrorPath{"hero", "heroFriends", 1, "name"} 98 | } 99 | }) 100 | }; 101 | 102 | // add test for github issue #230 : https://github.com/graphql-dotnet/graphql-client/issues/230 103 | yield return new object[] { 104 | "{\"data\":{\"getMyModelType\":{\"id\":\"foo\",\"title\":\"The best Foo movie!\"}}}", 105 | new GraphQLResponse { 106 | Data = new GetMyModelTypeResponse 107 | { 108 | getMyModelType = new Movie 109 | { 110 | id = "foo", 111 | title = "The best Foo movie!" 112 | } 113 | }, 114 | } 115 | }; 116 | } 117 | 118 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 119 | 120 | private GraphQLResponse NewAnonymouslyTypedGraphQLResponse(T data, GraphQLError[]? errors = null, Map? extensions = null) 121 | => new GraphQLResponse { Data = data, Errors = errors, Extensions = extensions }; 122 | } 123 | 124 | public class Friend 125 | { 126 | public string Id { get; set; } 127 | public string? Name { get; set; } 128 | } 129 | 130 | public class GetMyModelTypeResponse 131 | { 132 | //--- Properties --- 133 | public Movie getMyModelType { get; set; } 134 | } 135 | public class Movie 136 | { 137 | //--- Properties --- 138 | public string id { get; set; } 139 | public string title { get; set; } 140 | } 141 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToBytesTestData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using GraphQL.Client.Abstractions.Websocket; 3 | 4 | namespace GraphQL.Client.Serializer.Tests.TestData; 5 | 6 | public class SerializeToBytesTestData : IEnumerable 7 | { 8 | public IEnumerator GetEnumerator() 9 | { 10 | yield return new object[] { 11 | "{\"id\":\"1234567\",\"type\":\"start\",\"payload\":{\"query\":\"simplequerystring\",\"variables\":null,\"operationName\":null,\"extensions\":null}}", 12 | new GraphQLWebSocketRequest { 13 | Id = "1234567", 14 | Type = GraphQLWebSocketMessageType.GQL_START, 15 | Payload = new GraphQLRequest("simplequerystring") 16 | } 17 | }; 18 | yield return new object[] { 19 | "{\"id\":\"34476567\",\"type\":\"start\",\"payload\":{\"query\":\"simplequerystring\",\"variables\":{\"camelCaseProperty\":\"camelCase\",\"PascalCaseProperty\":\"PascalCase\"},\"operationName\":null,\"extensions\":null}}", 20 | new GraphQLWebSocketRequest { 21 | Id = "34476567", 22 | Type = GraphQLWebSocketMessageType.GQL_START, 23 | Payload = new GraphQLRequest("simple query string", new { camelCaseProperty = "camelCase", PascalCaseProperty = "PascalCase"}) 24 | } 25 | 26 | }; 27 | } 28 | 29 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 30 | } 31 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Serializer.Tests/TestData/SerializeToStringTestData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace GraphQL.Client.Serializer.Tests.TestData; 4 | 5 | public class SerializeToStringTestData : IEnumerable 6 | { 7 | public IEnumerator GetEnumerator() 8 | { 9 | yield return new object[] { 10 | "{\"query\":\"simplequerystring\",\"variables\":null,\"operationName\":null,\"extensions\":null}", 11 | new GraphQLRequest("simple query string") 12 | }; 13 | yield return new object[] { 14 | "{\"query\":\"simplequerystring\",\"variables\":null,\"operationName\":null,\"extensions\":{\"a\":\"abc\",\"b\":true,\"c\":{\"d\":42}}}", 15 | new GraphQLRequest("simple query string", extensions: new Dictionary { ["a"] = "abc", ["b"] = true, ["c"] = new Dictionary { ["d"] = 42 } }) 16 | }; 17 | yield return new object[] { 18 | "{\"query\":\"simplequerystring\",\"variables\":{\"camelCaseProperty\":\"camelCase\",\"PascalCaseProperty\":\"PascalCase\"},\"operationName\":null,\"extensions\":null}", 19 | new GraphQLRequest("simple query string", new { camelCaseProperty = "camelCase", PascalCaseProperty = "PascalCase"}) 20 | }; 21 | yield return new object[] { 22 | "{\"query\":\"simplequerystring\",\"variables\":null,\"operationName\":null,\"extensions\":null,\"authentication\":\"an-authentication-token\"}", 23 | new GraphQLRequest("simple query string"){{"authentication", "an-authentication-token"}} 24 | }; 25 | yield return new object[] { 26 | "{\"query\":\"enumtest\",\"variables\":{\"enums\":[\"REGULAR\",\"PASCAL_CASE\",\"CAMEL_CASE\",\"LOWER\",\"UPPER\",\"CONSTANT_CASE\"]},\"operationName\":null,\"extensions\":null}", 27 | new GraphQLRequest("enumtest", new { enums = Enum.GetValues(typeof(TestEnum)).Cast()}) 28 | }; 29 | } 30 | 31 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 32 | 33 | public enum TestEnum 34 | { 35 | Regular, 36 | PascalCase, 37 | camelCase, 38 | lower, 39 | UPPER, 40 | CONSTANT_CASE 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/Chat/AddMessageMutationResult.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Client.Tests.Common.Chat; 2 | 3 | public class AddMessageMutationResult 4 | { 5 | public AddMessageContent AddMessage { get; set; } 6 | 7 | public class AddMessageContent 8 | { 9 | public string Content { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/Chat/AddMessageVariables.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Client.Tests.Common.Chat; 2 | 3 | public class AddMessageVariables 4 | { 5 | public AddMessageInput Input { get; set; } 6 | 7 | public class AddMessageInput 8 | { 9 | public string FromId { get; set; } 10 | 11 | public string Content { get; set; } 12 | 13 | public string SentAt { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/Chat/GraphQLClientChatExtensions.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Client.Abstractions; 2 | 3 | namespace GraphQL.Client.Tests.Common.Chat; 4 | 5 | public static class GraphQLClientChatExtensions 6 | { 7 | public const string ADD_MESSAGE_QUERY = 8 | @"mutation($input: MessageInputType){ 9 | addMessage(message: $input){ 10 | content 11 | } 12 | }"; 13 | 14 | public static Task> AddMessageAsync(this IGraphQLClient client, string message) 15 | { 16 | var variables = new AddMessageVariables 17 | { 18 | Input = new AddMessageVariables.AddMessageInput 19 | { 20 | FromId = "2", 21 | Content = message, 22 | SentAt = DateTime.Now.ToString("s") 23 | } 24 | }; 25 | 26 | var graphQLRequest = new GraphQLRequest(ADD_MESSAGE_QUERY, variables); 27 | return client.SendMutationAsync(graphQLRequest); 28 | } 29 | 30 | public static Task> JoinDeveloperUser(this IGraphQLClient client) 31 | { 32 | var graphQLRequest = new GraphQLRequest(@" 33 | mutation($userId: String){ 34 | join(userId: $userId){ 35 | displayName 36 | id 37 | } 38 | }", 39 | new 40 | { 41 | userId = "1" 42 | }); 43 | return client.SendMutationAsync(graphQLRequest); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/Chat/JoinDeveloperMutationResult.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Client.Tests.Common.Chat; 2 | 3 | public class JoinDeveloperMutationResult 4 | { 5 | public JoinContent Join { get; set; } 6 | 7 | public class JoinContent 8 | { 9 | public string DisplayName { get; set; } 10 | 11 | public string Id { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/Chat/Schema/CapitalizedFieldsGraphType.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | 3 | namespace GraphQL.Client.Tests.Common.Chat.Schema; 4 | 5 | public class CapitalizedFieldsGraphType : ObjectGraphType 6 | { 7 | public CapitalizedFieldsGraphType() 8 | { 9 | Name = "CapitalizedFields"; 10 | 11 | Field("StringField") 12 | .Resolve(context => "hello world"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatMutation.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | 3 | namespace GraphQL.Client.Tests.Common.Chat.Schema; 4 | 5 | public class ChatMutation : ObjectGraphType 6 | { 7 | public ChatMutation(IChat chat) 8 | { 9 | Field("addMessage") 10 | .Argument("message") 11 | .Resolve(context => 12 | { 13 | var receivedMessage = context.GetArgument("message"); 14 | var message = chat.AddMessage(receivedMessage); 15 | return message; 16 | }); 17 | 18 | Field("join") 19 | .Argument("userId") 20 | .Resolve(context => 21 | { 22 | var userId = context.GetArgument("userId"); 23 | var userJoined = chat.Join(userId); 24 | return userJoined; 25 | }); 26 | } 27 | } 28 | 29 | public class MessageInputType : InputObjectGraphType 30 | { 31 | public MessageInputType() 32 | { 33 | Field("fromId"); 34 | Field("content"); 35 | Field("sentAt"); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatQuery.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace GraphQL.Client.Tests.Common.Chat.Schema; 6 | 7 | public class ChatQuery : ObjectGraphType 8 | { 9 | private readonly IServiceProvider _serviceProvider; 10 | 11 | public static readonly Dictionary TestExtensions = new() 12 | { 13 | {"extension1", "hello world"}, 14 | {"another extension", 4711}, 15 | {"long", 19942590700} 16 | }; 17 | 18 | // properties for unit testing 19 | 20 | public readonly ManualResetEventSlim LongRunningQueryBlocker = new ManualResetEventSlim(); 21 | public readonly ManualResetEventSlim WaitingOnQueryBlocker = new ManualResetEventSlim(); 22 | 23 | public ChatQuery(IChat chat, IServiceProvider serviceProvider) 24 | { 25 | _serviceProvider = serviceProvider; 26 | Name = "ChatQuery"; 27 | 28 | Field>("messages").Resolve(context => chat.AllMessages.Take(100)); 29 | 30 | Field("extensionsTest") 31 | .Resolve(context => 32 | { 33 | context.Errors.Add(new ExecutionError("this error contains extension fields", TestExtensions)); 34 | return null; 35 | }); 36 | 37 | Field("longRunning") 38 | .Resolve(context => 39 | { 40 | WaitingOnQueryBlocker.Set(); 41 | LongRunningQueryBlocker.Wait(); 42 | WaitingOnQueryBlocker.Reset(); 43 | return "finally returned"; 44 | }); 45 | 46 | Field("clientUserAgent") 47 | .Resolve(context => 48 | { 49 | var contextAccessor = _serviceProvider.GetRequiredService(); 50 | if (!contextAccessor.HttpContext.Request.Headers.UserAgent.Any()) 51 | { 52 | context.Errors.Add(new ExecutionError("user agent header not set")); 53 | return null; 54 | } 55 | return contextAccessor.HttpContext.Request.Headers.UserAgent.ToString(); 56 | }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatSchema.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace GraphQL.Client.Tests.Common.Chat.Schema; 4 | 5 | public class ChatSchema : Types.Schema 6 | { 7 | public ChatSchema(IServiceProvider services) 8 | : base(services) 9 | { 10 | Query = services.GetRequiredService(); 11 | Mutation = services.GetRequiredService(); 12 | Subscription = services.GetRequiredService(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/Chat/Schema/ChatSubscriptions.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Linq; 2 | using GraphQL.Resolvers; 3 | using GraphQL.Types; 4 | 5 | namespace GraphQL.Client.Tests.Common.Chat.Schema; 6 | 7 | public class ChatSubscriptions : ObjectGraphType 8 | { 9 | private readonly IChat _chat; 10 | 11 | public ChatSubscriptions(IChat chat) 12 | { 13 | _chat = chat; 14 | AddField(new FieldType 15 | { 16 | Name = "messageAdded", 17 | Type = typeof(MessageType), 18 | Resolver = new FuncFieldResolver(ResolveMessage), 19 | StreamResolver = new SourceStreamResolver(Subscribe) 20 | }); 21 | 22 | AddField(new FieldType 23 | { 24 | Name = "contentAdded", 25 | Type = typeof(MessageType), 26 | Resolver = new FuncFieldResolver(ResolveMessage), 27 | StreamResolver = new SourceStreamResolver(Subscribe) 28 | }); 29 | 30 | AddField(new FieldType 31 | { 32 | Name = "messageAddedByUser", 33 | Arguments = new QueryArguments( 34 | new QueryArgument> { Name = "id" } 35 | ), 36 | Type = typeof(MessageType), 37 | Resolver = new FuncFieldResolver(ResolveMessage), 38 | StreamResolver = new SourceStreamResolver(SubscribeById) 39 | }); 40 | 41 | AddField(new FieldType 42 | { 43 | Name = "userJoined", 44 | Type = typeof(MessageFromType), 45 | Resolver = new FuncFieldResolver(context => context.Source as MessageFrom), 46 | StreamResolver = new SourceStreamResolver(context => _chat.UserJoined()) 47 | }); 48 | 49 | 50 | AddField(new FieldType 51 | { 52 | Name = "failImmediately", 53 | Type = typeof(MessageType), 54 | Resolver = new FuncFieldResolver(ResolveMessage), 55 | StreamResolver = new SourceStreamResolver((Func>)(context => throw new NotSupportedException("this is supposed to fail"))) 56 | }); 57 | } 58 | 59 | private IObservable SubscribeById(IResolveFieldContext context) 60 | { 61 | var user = context.User; 62 | 63 | var sub = "Anonymous"; 64 | if (user != null) 65 | sub = user.Claims.FirstOrDefault(c => c.Type == "sub")?.Value; 66 | 67 | var messages = _chat.Messages(sub); 68 | 69 | var id = context.GetArgument("id"); 70 | return messages.Where(message => message.From.Id == id); 71 | } 72 | 73 | private Message ResolveMessage(IResolveFieldContext context) 74 | { 75 | var message = context.Source as Message; 76 | 77 | return message; 78 | } 79 | 80 | private IObservable Subscribe(IResolveFieldContext context) 81 | { 82 | var user = context.User; 83 | 84 | var sub = "Anonymous"; 85 | if (user != null) 86 | sub = user.Claims.FirstOrDefault(c => c.Type == "sub")?.Value; 87 | 88 | return _chat.Messages(sub); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/Chat/Schema/IChat.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Diagnostics; 3 | using System.Reactive.Disposables; 4 | using System.Reactive.Linq; 5 | using System.Reactive.Subjects; 6 | 7 | namespace GraphQL.Client.Tests.Common.Chat.Schema; 8 | 9 | public interface IChat 10 | { 11 | ConcurrentStack AllMessages { get; } 12 | 13 | Message AddMessage(Message message); 14 | 15 | MessageFrom Join(string userId); 16 | 17 | IObservable Messages(string user); 18 | IObservable UserJoined(); 19 | 20 | Message AddMessage(ReceivedMessage message); 21 | } 22 | 23 | public class Chat : IChat 24 | { 25 | private readonly ISubject _messageStream = new ReplaySubject(1); 26 | private readonly ISubject _userJoined = new Subject(); 27 | 28 | public Chat() 29 | { 30 | AllMessages = new ConcurrentStack(); 31 | Users = new ConcurrentDictionary 32 | { 33 | ["1"] = "developer", 34 | ["2"] = "tester" 35 | }; 36 | } 37 | 38 | public ConcurrentDictionary Users { get; private set; } 39 | 40 | public ConcurrentStack AllMessages { get; private set; } 41 | 42 | public Message AddMessage(ReceivedMessage message) 43 | { 44 | if (!Users.TryGetValue(message.FromId, out var displayName)) 45 | { 46 | displayName = "(unknown)"; 47 | } 48 | 49 | return AddMessage(new Message 50 | { 51 | Content = message.Content, 52 | SentAt = message.SentAt, 53 | From = new MessageFrom 54 | { 55 | DisplayName = displayName, 56 | Id = message.FromId 57 | } 58 | }); 59 | } 60 | 61 | public Message AddMessage(Message message) 62 | { 63 | AllMessages.Push(message); 64 | _messageStream.OnNext(message); 65 | return message; 66 | } 67 | 68 | public MessageFrom Join(string userId) 69 | { 70 | if (!Users.TryGetValue(userId, out var displayName)) 71 | { 72 | displayName = "(unknown)"; 73 | } 74 | 75 | var joinedUser = new MessageFrom 76 | { 77 | Id = userId, 78 | DisplayName = displayName 79 | }; 80 | 81 | _userJoined.OnNext(joinedUser); 82 | return joinedUser; 83 | } 84 | 85 | public IObservable Messages(string user) => 86 | Observable.Create(observer => 87 | { 88 | Debug.WriteLine($"creating messages stream for user '{user}' on thread {Thread.CurrentThread.ManagedThreadId}"); 89 | return new CompositeDisposable 90 | { 91 | _messageStream.Select(message => 92 | { 93 | message.Sub = user; 94 | return message; 95 | }) 96 | .Subscribe(observer), 97 | Disposable.Create(() => Debug.WriteLine($"disposing messages stream for user '{user}' on thread {Thread.CurrentThread.ManagedThreadId}")) 98 | }; 99 | }); 100 | 101 | public void AddError(Exception exception) => _messageStream.OnError(exception); 102 | 103 | public IObservable UserJoined() => _userJoined.AsObservable(); 104 | } 105 | 106 | public class User 107 | { 108 | public string Id { get; set; } 109 | public string Name { get; set; } 110 | } 111 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/Chat/Schema/Message.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Client.Tests.Common.Chat.Schema; 2 | 3 | public class Message 4 | { 5 | public MessageFrom From { get; set; } 6 | 7 | public string Sub { get; set; } 8 | 9 | public string Content { get; set; } 10 | 11 | public DateTime SentAt { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/Chat/Schema/MessageFrom.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Client.Tests.Common.Chat.Schema; 2 | 3 | public class MessageFrom 4 | { 5 | public string Id { get; set; } 6 | 7 | public string DisplayName { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/Chat/Schema/MessageFromType.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | 3 | namespace GraphQL.Client.Tests.Common.Chat.Schema; 4 | 5 | public class MessageFromType : ObjectGraphType 6 | { 7 | public MessageFromType() 8 | { 9 | Field(o => o.Id); 10 | Field(o => o.DisplayName); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/Chat/Schema/MessageType.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | 3 | namespace GraphQL.Client.Tests.Common.Chat.Schema; 4 | 5 | public class MessageType : ObjectGraphType 6 | { 7 | public MessageType() 8 | { 9 | Field(o => o.Content); 10 | Field(o => o.SentAt); 11 | Field(o => o.Sub); 12 | Field(o => o.From, false, typeof(MessageFromType)).Resolve(ResolveFrom); 13 | } 14 | 15 | private MessageFrom ResolveFrom(IResolveFieldContext context) 16 | { 17 | var message = context.Source; 18 | return message.From; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/Chat/Schema/ReceivedMessage.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Client.Tests.Common.Chat.Schema; 2 | 3 | public class ReceivedMessage 4 | { 5 | public string FromId { get; set; } 6 | 7 | public string Content { get; set; } 8 | 9 | public DateTime SentAt { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/Common.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Client.Tests.Common.Chat.Schema; 2 | using GraphQL.Client.Tests.Common.StarWars; 3 | using GraphQL.Client.Tests.Common.StarWars.Types; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace GraphQL.Client.Tests.Common; 7 | 8 | public static class Common 9 | { 10 | public const string STAR_WARS_ENDPOINT = "/graphql/starwars"; 11 | public const string CHAT_ENDPOINT = "/graphql/chat"; 12 | 13 | public static StarWarsSchema GetStarWarsSchema() 14 | { 15 | var services = new ServiceCollection(); 16 | services.AddStarWarsSchema(); 17 | return services.BuildServiceProvider().GetRequiredService(); 18 | } 19 | 20 | public static ChatSchema GetChatSchema() 21 | { 22 | var services = new ServiceCollection(); 23 | services.AddChatSchema(); 24 | return services.BuildServiceProvider().GetRequiredService(); 25 | } 26 | 27 | public static void AddStarWarsSchema(this IServiceCollection services) 28 | { 29 | services.AddSingleton(); 30 | services.AddSingleton(); 31 | services.AddSingleton(); 32 | services.AddSingleton(); 33 | services.AddTransient(); 34 | services.AddTransient(); 35 | services.AddTransient(); 36 | services.AddTransient(); 37 | services.AddTransient(); 38 | } 39 | 40 | public static void AddChatSchema(this IServiceCollection services) 41 | { 42 | var chat = new Chat.Schema.Chat(); 43 | services.AddSingleton(chat); 44 | services.AddSingleton(chat); 45 | services.AddSingleton(); 46 | services.AddSingleton(); 47 | services.AddSingleton(); 48 | services.AddSingleton(); 49 | services.AddSingleton(); 50 | services.AddSingleton(); 51 | services.AddSingleton(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/GraphQL.Client.Tests.Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/Helpers/AvailableJsonSerializers.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using GraphQL.Client.Abstractions; 3 | 4 | namespace GraphQL.Client.Tests.Common.Helpers; 5 | 6 | public class AvailableJsonSerializers : IEnumerable where TSerializerInterface : IGraphQLJsonSerializer 7 | { 8 | public IEnumerator GetEnumerator() 9 | { 10 | // try to find one in the assembly and assign that 11 | var type = typeof(TSerializerInterface); 12 | return AppDomain.CurrentDomain 13 | .GetAssemblies() 14 | .SelectMany(s => s.GetTypes()) 15 | .Where(p => type.IsAssignableFrom(p) && !p.IsInterface && !p.IsAbstract) 16 | .Select(serializerType => new object[] { Activator.CreateInstance(serializerType) }) 17 | .GetEnumerator(); 18 | } 19 | 20 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 21 | } 22 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/Helpers/CallbackMonitor.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using FluentAssertions; 3 | using FluentAssertions.Execution; 4 | using FluentAssertions.Primitives; 5 | 6 | namespace GraphQL.Client.Tests.Common.Helpers; 7 | 8 | public class CallbackMonitor 9 | { 10 | private readonly ManualResetEventSlim _callbackInvoked = new ManualResetEventSlim(); 11 | 12 | /// 13 | /// The timeout for . Defaults to 1 second. 14 | /// 15 | public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(1); 16 | 17 | /// 18 | /// Indicates that an update has been received since the last 19 | /// 20 | public bool CallbackInvoked => _callbackInvoked.IsSet; 21 | 22 | /// 23 | /// The last payload which was received. 24 | /// 25 | public T LastPayload { get; private set; } 26 | 27 | public void Invoke(T param) 28 | { 29 | LastPayload = param; 30 | Debug.WriteLine($"CallbackMonitor invoke handler thread id: {Thread.CurrentThread.ManagedThreadId}"); 31 | _callbackInvoked.Set(); 32 | } 33 | 34 | /// 35 | /// Resets the tester class. Should be called before triggering the potential update 36 | /// 37 | public void Reset() 38 | { 39 | LastPayload = default; 40 | _callbackInvoked.Reset(); 41 | } 42 | 43 | public CallbackAssertions Should() => new CallbackAssertions(this); 44 | 45 | public class CallbackAssertions : ReferenceTypeAssertions, CallbackAssertions> 46 | { 47 | public CallbackAssertions(CallbackMonitor tester) : base(tester) 48 | { } 49 | 50 | protected override string Identifier => "callback"; 51 | 52 | public AndWhichConstraint, TPayload> HaveBeenInvokedWithPayload(TimeSpan timeout, 53 | string because = "", params object[] becauseArgs) 54 | { 55 | Execute.Assertion 56 | .BecauseOf(because, becauseArgs) 57 | .Given(() => 58 | { 59 | Debug.WriteLine($"HaveBeenInvokedWithPayload thread id: {Thread.CurrentThread.ManagedThreadId}"); 60 | return Subject._callbackInvoked.Wait(timeout); 61 | }) 62 | .ForCondition(isSet => isSet) 63 | .FailWith("Expected {context:callback} to be invoked{reason}, but did not receive a call within {0}", timeout); 64 | 65 | Subject._callbackInvoked.Reset(); 66 | return new AndWhichConstraint, TPayload>(this, Subject.LastPayload); 67 | } 68 | public AndWhichConstraint, TPayload> HaveBeenInvokedWithPayload(string because = "", params object[] becauseArgs) 69 | => HaveBeenInvokedWithPayload(Subject.Timeout, because, becauseArgs); 70 | 71 | public AndConstraint> HaveBeenInvoked(TimeSpan timeout, string because = "", params object[] becauseArgs) 72 | => HaveBeenInvokedWithPayload(timeout, because, becauseArgs); 73 | public AndConstraint> HaveBeenInvoked(string because = "", params object[] becauseArgs) 74 | => HaveBeenInvokedWithPayload(Subject.Timeout, because, becauseArgs); 75 | 76 | public AndConstraint> NotHaveBeenInvoked(TimeSpan timeout, 77 | string because = "", params object[] becauseArgs) 78 | { 79 | Execute.Assertion 80 | .BecauseOf(because, becauseArgs) 81 | .Given(() => Subject._callbackInvoked.Wait(timeout)) 82 | .ForCondition(isSet => !isSet) 83 | .FailWith("Expected {context:callback} to not be invoked{reason}, but did receive a call: {0}", Subject.LastPayload); 84 | 85 | Subject._callbackInvoked.Reset(); 86 | return new AndConstraint>(this); 87 | } 88 | public AndConstraint> NotHaveBeenInvoked(string because = "", params object[] becauseArgs) 89 | => NotHaveBeenInvoked(TimeSpan.FromMilliseconds(100), because, becauseArgs); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/Helpers/ConcurrentTaskWrapper.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Client.Tests.Common.Helpers; 2 | 3 | public class ConcurrentTaskWrapper 4 | { 5 | public static ConcurrentTaskWrapper New(Func> createTask) => new(createTask); 6 | 7 | private readonly Func _createTask; 8 | private Task _internalTask; 9 | 10 | public ConcurrentTaskWrapper(Func createTask) 11 | { 12 | _createTask = createTask; 13 | } 14 | 15 | public Task Invoke() 16 | { 17 | if (_internalTask != null) 18 | return _internalTask; 19 | 20 | return _internalTask = _createTask(); 21 | } 22 | } 23 | 24 | public class ConcurrentTaskWrapper 25 | { 26 | private readonly Func> _createTask; 27 | private Task _internalTask; 28 | 29 | public ConcurrentTaskWrapper(Func> createTask) 30 | { 31 | _createTask = createTask; 32 | } 33 | 34 | public Task Invoke() 35 | { 36 | if (_internalTask != null) 37 | return _internalTask; 38 | 39 | return _internalTask = _createTask(); 40 | } 41 | 42 | public void Start() => _internalTask ??= _createTask(); 43 | 44 | public Func> Invoking() => Invoke; 45 | 46 | public void Clear() => _internalTask = null; 47 | } 48 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/Helpers/MiscellaneousExtensions.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Client.Http; 2 | 3 | namespace GraphQL.Client.Tests.Common.Helpers; 4 | 5 | public static class MiscellaneousExtensions 6 | { 7 | public static string RemoveWhitespace(this string input) => 8 | new string(input.ToCharArray() 9 | .Where(c => !char.IsWhiteSpace(c)) 10 | .ToArray()); 11 | 12 | public static CallbackMonitor ConfigureMonitorForOnWebsocketConnected( 13 | this GraphQLHttpClient client) 14 | { 15 | var tester = new CallbackMonitor(); 16 | client.Options.OnWebsocketConnected = c => 17 | { 18 | tester.Invoke(c); 19 | return Task.CompletedTask; 20 | }; 21 | return tester; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/Helpers/NetworkHelpers.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Sockets; 3 | 4 | namespace GraphQL.Client.Tests.Common.Helpers; 5 | 6 | public static class NetworkHelpers 7 | { 8 | public static int GetFreeTcpPortNumber() 9 | { 10 | var l = new TcpListener(IPAddress.Loopback, 0); 11 | l.Start(); 12 | var port = ((IPEndPoint)l.LocalEndpoint).Port; 13 | l.Stop(); 14 | return port; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "GraphQL.Client.Tests.Common": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "applicationUrl": "http://localhost:59034" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/StarWars/Extensions/ResolveFieldContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Builders; 2 | using GraphQL.Client.Tests.Common.StarWars.Types; 3 | using GraphQL.Types.Relay.DataObjects; 4 | 5 | namespace GraphQL.Client.Tests.Common.StarWars.Extensions; 6 | 7 | public static class ResolveFieldContextExtensions 8 | { 9 | public static Connection GetPagedResults(this IResolveConnectionContext context, StarWarsData data, List ids) where U : StarWarsCharacter 10 | { 11 | List idList; 12 | List list; 13 | string cursor; 14 | string endCursor; 15 | var pageSize = context.PageSize ?? 20; 16 | 17 | if (context.IsUnidirectional || context.After != null || context.Before == null) 18 | { 19 | if (context.After != null) 20 | { 21 | idList = ids 22 | .SkipWhile(x => !Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(x)).Equals(context.After)) 23 | .Take(context.First ?? pageSize).ToList(); 24 | } 25 | else 26 | { 27 | idList = ids 28 | .Take(context.First ?? pageSize).ToList(); 29 | } 30 | } 31 | else 32 | { 33 | if (context.Before != null) 34 | { 35 | idList = ids.Reverse() 36 | .SkipWhile(x => !Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(x)).Equals(context.Before)) 37 | .Take(context.Last ?? pageSize).ToList(); 38 | } 39 | else 40 | { 41 | idList = ids.Reverse() 42 | .Take(context.Last ?? pageSize).ToList(); 43 | } 44 | } 45 | 46 | list = data.GetCharactersAsync(idList).Result as List ?? throw new InvalidOperationException($"GetCharactersAsync method should return list of '{typeof(U).Name}' items."); 47 | cursor = list.Count > 0 ? list.Last().Cursor : null; 48 | endCursor = ids.Count > 0 ? Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(ids.Last())) : null; 49 | 50 | return new Connection 51 | { 52 | Edges = list.Select(x => new Edge { Cursor = x.Cursor, Node = x }).ToList(), 53 | TotalCount = list.Count, 54 | PageInfo = new PageInfo 55 | { 56 | EndCursor = endCursor, 57 | HasNextPage = endCursor == null ? false : cursor != endCursor, 58 | } 59 | }; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/StarWars/StarWarsData.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Client.Tests.Common.StarWars.Types; 2 | 3 | namespace GraphQL.Client.Tests.Common.StarWars; 4 | 5 | public class StarWarsData 6 | { 7 | private readonly List _characters = new List(); 8 | 9 | public StarWarsData() 10 | { 11 | _characters.Add(new Human 12 | { 13 | Id = "1", 14 | Name = "Luke", 15 | Friends = new List { "3", "4" }, 16 | AppearsIn = new[] { 4, 5, 6 }, 17 | HomePlanet = "Tatooine", 18 | Cursor = "MQ==" 19 | }); 20 | _characters.Add(new Human 21 | { 22 | Id = "2", 23 | Name = "Vader", 24 | AppearsIn = new[] { 4, 5, 6 }, 25 | HomePlanet = "Tatooine", 26 | Cursor = "Mg==" 27 | }); 28 | 29 | _characters.Add(new Droid 30 | { 31 | Id = "3", 32 | Name = "R2-D2", 33 | Friends = new List { "1", "4" }, 34 | AppearsIn = new[] { 4, 5, 6 }, 35 | PrimaryFunction = "Astromech", 36 | Cursor = "Mw==" 37 | }); 38 | _characters.Add(new Droid 39 | { 40 | Id = "4", 41 | Name = "C-3PO", 42 | AppearsIn = new[] { 4, 5, 6 }, 43 | PrimaryFunction = "Protocol", 44 | Cursor = "NA==" 45 | }); 46 | } 47 | 48 | public IEnumerable GetFriends(StarWarsCharacter character) 49 | { 50 | if (character == null) 51 | { 52 | return null; 53 | } 54 | 55 | var friends = new List(); 56 | var lookup = character.Friends; 57 | if (lookup != null) 58 | { 59 | foreach (var c in _characters.Where(h => lookup.Contains(h.Id))) 60 | friends.Add(c); 61 | } 62 | return friends; 63 | } 64 | 65 | public StarWarsCharacter AddCharacter(StarWarsCharacter character) 66 | { 67 | character.Id = _characters.Count.ToString(); 68 | _characters.Add(character); 69 | return character; 70 | } 71 | 72 | public Task GetHumanByIdAsync(string id) 73 | { 74 | return Task.FromResult(_characters.FirstOrDefault(h => h.Id == id && h is Human) as Human); 75 | } 76 | 77 | public Task GetDroidByIdAsync(string id) 78 | { 79 | return Task.FromResult(_characters.FirstOrDefault(h => h.Id == id && h is Droid) as Droid); 80 | } 81 | 82 | public Task> GetCharactersAsync(List guids) 83 | { 84 | return Task.FromResult(_characters.Where(c => guids.Contains(c.Id)).ToList()); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/StarWars/StarWarsMutation.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Client.Tests.Common.StarWars.Types; 2 | using GraphQL.Types; 3 | 4 | namespace GraphQL.Client.Tests.Common.StarWars; 5 | 6 | /// Mutation graph type for StarWars schema. 7 | /// 8 | /// This is an example JSON request for a mutation 9 | /// { 10 | /// "query": "mutation ($human:HumanInput!){ createHuman(human: $human) { id name } }", 11 | /// "variables": { 12 | /// "human": { 13 | /// "name": "Boba Fett" 14 | /// } 15 | /// } 16 | /// } 17 | /// 18 | public class StarWarsMutation : ObjectGraphType 19 | { 20 | public StarWarsMutation(StarWarsData data) 21 | { 22 | Name = "Mutation"; 23 | 24 | Field("createHuman") 25 | .Argument>("human") 26 | .Resolve(context => 27 | { 28 | var human = context.GetArgument("human"); 29 | return data.AddCharacter(human); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/StarWars/StarWarsQuery.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Client.Tests.Common.StarWars.Types; 2 | using GraphQL.Types; 3 | 4 | namespace GraphQL.Client.Tests.Common.StarWars; 5 | 6 | public class StarWarsQuery : ObjectGraphType 7 | { 8 | public StarWarsQuery(StarWarsData data) 9 | { 10 | Name = "Query"; 11 | 12 | Field("hero").ResolveAsync(async context => await data.GetDroidByIdAsync("3")); 13 | Field("human") 14 | .Argument>("id", "id of the human") 15 | .ResolveAsync(async context => await data.GetHumanByIdAsync(context.GetArgument("id")) 16 | ); 17 | 18 | Func> func = (context, id) => data.GetDroidByIdAsync(id); 19 | 20 | Field("droid") 21 | .Argument>("id", "id of the droid") 22 | .ResolveDelegate(func); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/StarWars/StarWarsSchema.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace GraphQL.Client.Tests.Common.StarWars; 5 | 6 | public class StarWarsSchema : Schema 7 | { 8 | public StarWarsSchema(IServiceProvider serviceProvider) 9 | : base(serviceProvider) 10 | { 11 | Query = serviceProvider.GetRequiredService(); 12 | Mutation = serviceProvider.GetRequiredService(); 13 | 14 | Description = "Example StarWars universe schema"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/StarWars/TestData/StarWarsHumans.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace GraphQL.Client.Tests.Common.StarWars.TestData; 4 | 5 | /// 6 | /// Test data object 7 | /// 8 | public class StarWarsHumans : IEnumerable 9 | { 10 | public IEnumerator GetEnumerator() 11 | { 12 | yield return new object[] { 1, "Luke" }; 13 | yield return new object[] { 2, "Vader" }; 14 | } 15 | 16 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 17 | } 18 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/StarWars/Types/CharacterInterface.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | using GraphQL.Types.Relay; 3 | 4 | namespace GraphQL.Client.Tests.Common.StarWars.Types; 5 | 6 | public class CharacterInterface : InterfaceGraphType 7 | { 8 | public CharacterInterface() 9 | { 10 | Name = "Character"; 11 | 12 | Field>("id").Description("The id of the character."); 13 | Field("name").Description("The name of the character."); 14 | 15 | Field>("friends"); 16 | Field>>("friendsConnection"); 17 | Field>("appearsIn").Description("Which movie they appear in."); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/StarWars/Types/DroidType.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Client.Tests.Common.StarWars.Extensions; 2 | using GraphQL.Types; 3 | 4 | namespace GraphQL.Client.Tests.Common.StarWars.Types; 5 | 6 | public class DroidType : ObjectGraphType 7 | { 8 | public DroidType(StarWarsData data) 9 | { 10 | Name = "Droid"; 11 | Description = "A mechanical creature in the Star Wars universe."; 12 | 13 | Field>("id").Description("The id of the droid.").Resolve(context => context.Source.Id); 14 | Field("name").Description("The name of the droid.").Resolve(context => context.Source.Name); 15 | 16 | Field>("friends").Resolve(context => data.GetFriends(context.Source)); 17 | 18 | Connection("friendsConnection") 19 | .Description("A list of a character's friends.") 20 | .Bidirectional() 21 | .Resolve(context => context.GetPagedResults(data, context.Source.Friends)); 22 | 23 | Field>("appearsIn").Description("Which movie they appear in."); 24 | Field("primaryFunction").Description("The primary function of the droid.").Resolve(context => context.Source.PrimaryFunction); 25 | 26 | Interface(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/StarWars/Types/EpisodeEnum.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | 3 | namespace GraphQL.Client.Tests.Common.StarWars.Types; 4 | 5 | public class EpisodeEnum : EnumerationGraphType 6 | { 7 | public EpisodeEnum() 8 | { 9 | Name = "Episode"; 10 | Description = "One of the films in the Star Wars Trilogy."; 11 | Add("NEWHOPE", 4, "Released in 1977."); 12 | Add("EMPIRE", 5, "Released in 1980."); 13 | Add("JEDI", 6, "Released in 1983."); 14 | } 15 | } 16 | 17 | public enum Episodes 18 | { 19 | NEWHOPE = 4, 20 | EMPIRE = 5, 21 | JEDI = 6 22 | } 23 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/StarWars/Types/HumanInputType.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | 3 | namespace GraphQL.Client.Tests.Common.StarWars.Types; 4 | 5 | public class HumanInputType : InputObjectGraphType 6 | { 7 | public HumanInputType() 8 | { 9 | Name = "HumanInput"; 10 | Field>("name"); 11 | Field("homePlanet"); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/StarWars/Types/HumanType.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Client.Tests.Common.StarWars.Extensions; 2 | using GraphQL.Types; 3 | 4 | namespace GraphQL.Client.Tests.Common.StarWars.Types; 5 | 6 | public class HumanType : ObjectGraphType 7 | { 8 | public HumanType(StarWarsData data) 9 | { 10 | Name = "Human"; 11 | 12 | Field>("id").Description("The id of the human.").Resolve(context => context.Source.Id); 13 | Field("name").Description("The name of the human.").Resolve(context => context.Source.Name); 14 | 15 | Field>("friends").Resolve(context => data.GetFriends(context.Source)); 16 | 17 | Connection("friendsConnection") 18 | .Description("A list of a character's friends.") 19 | .Bidirectional() 20 | .Resolve(context => context.GetPagedResults(data, context.Source.Friends)); 21 | 22 | Field>("appearsIn").Description("Which movie they appear in."); 23 | 24 | Field("homePlanet").Description("The home planet of the human.").Resolve(context => context.Source.HomePlanet); 25 | 26 | Interface(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/GraphQL.Client.Tests.Common/StarWars/Types/StarWarsCharacter.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Client.Tests.Common.StarWars.Types; 2 | 3 | public abstract class StarWarsCharacter 4 | { 5 | public string Id { get; set; } 6 | public string Name { get; set; } 7 | public List Friends { get; set; } 8 | public int[] AppearsIn { get; set; } 9 | public string Cursor { get; set; } 10 | } 11 | 12 | public class Human : StarWarsCharacter 13 | { 14 | public string HomePlanet { get; set; } 15 | } 16 | 17 | public class Droid : StarWarsCharacter 18 | { 19 | public string PrimaryFunction { get; set; } 20 | } 21 | -------------------------------------------------------------------------------- /tests/GraphQL.Integration.Tests/APQ/AutomaticPersistentQueriesTest.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using FluentAssertions; 3 | using GraphQL.Client.Abstractions; 4 | using GraphQL.Client.Http; 5 | using GraphQL.Client.Tests.Common.StarWars.TestData; 6 | using GraphQL.Integration.Tests.Helpers; 7 | using Xunit; 8 | 9 | namespace GraphQL.Integration.Tests.APQ; 10 | 11 | [SuppressMessage("ReSharper", "UseConfigureAwaitFalse")] 12 | public class AutomaticPersistentQueriesTest : IAsyncLifetime, IClassFixture 13 | { 14 | public SystemTextJsonAutoNegotiateServerTestFixture Fixture { get; } 15 | protected GraphQLHttpClient StarWarsClient; 16 | protected GraphQLHttpClient StarWarsWebsocketClient; 17 | 18 | public AutomaticPersistentQueriesTest(SystemTextJsonAutoNegotiateServerTestFixture fixture) 19 | { 20 | Fixture = fixture; 21 | } 22 | 23 | public async Task InitializeAsync() 24 | { 25 | await Fixture.CreateServer(); 26 | StarWarsClient = Fixture.GetStarWarsClient(options => options.EnableAutomaticPersistedQueries = _ => true); 27 | StarWarsWebsocketClient = Fixture.GetStarWarsClient(options => 28 | { 29 | options.EnableAutomaticPersistedQueries = _ => true; 30 | options.UseWebSocketForQueriesAndMutations = true; 31 | }); 32 | } 33 | 34 | public Task DisposeAsync() 35 | { 36 | StarWarsClient?.Dispose(); 37 | return Task.CompletedTask; 38 | } 39 | 40 | [Theory] 41 | [ClassData(typeof(StarWarsHumans))] 42 | public async void After_querying_all_starwars_humans_the_APQDisabledForSession_is_still_false_Async(int id, string name) 43 | { 44 | var query = new GraphQLQuery(""" 45 | query Human($id: String!){ 46 | human(id: $id) { 47 | name 48 | } 49 | } 50 | """); 51 | 52 | var graphQLRequest = new GraphQLRequest(query, new { id = id.ToString() }); 53 | 54 | var response = await StarWarsClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }); 55 | 56 | Assert.Null(response.Errors); 57 | Assert.Equal(name, response.Data.Human.Name); 58 | StarWarsClient.APQDisabledForSession.Should().BeFalse("if APQ has worked it won't get disabled"); 59 | } 60 | 61 | [Theory] 62 | [ClassData(typeof(StarWarsHumans))] 63 | public async void After_querying_all_starwars_humans_using_websocket_transport_the_APQDisabledForSession_is_still_false_Async(int id, string name) 64 | { 65 | var query = new GraphQLQuery(""" 66 | query Human($id: String!){ 67 | human(id: $id) { 68 | name 69 | } 70 | } 71 | """); 72 | 73 | var graphQLRequest = new GraphQLRequest(query, new { id = id.ToString() }); 74 | 75 | var response = await StarWarsWebsocketClient.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } }); 76 | 77 | Assert.Null(response.Errors); 78 | Assert.Equal(name, response.Data.Human.Name); 79 | StarWarsWebsocketClient.APQDisabledForSession.Should().BeFalse("if APQ has worked it won't get disabled"); 80 | } 81 | 82 | [Fact] 83 | public void Verify_the_persisted_query_extension_object() 84 | { 85 | var query = new GraphQLQuery(""" 86 | query Human($id: String!){ 87 | human(id: $id) { 88 | name 89 | } 90 | } 91 | """); 92 | query.Sha256Hash.Should().NotBeNullOrEmpty(); 93 | 94 | var request = new GraphQLRequest(query); 95 | request.Extensions.Should().BeNull(); 96 | request.GeneratePersistedQueryExtension(); 97 | request.Extensions.Should().NotBeNull(); 98 | 99 | string expectedKey = "persistedQuery"; 100 | var expectedExtensionValue = new Dictionary 101 | { 102 | ["version"] = 1, 103 | ["sha256Hash"] = query.Sha256Hash, 104 | }; 105 | 106 | request.Extensions.Should().ContainKey(expectedKey); 107 | request.Extensions![expectedKey].As>() 108 | .Should().NotBeNull().And.BeEquivalentTo(expectedExtensionValue); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tests/GraphQL.Integration.Tests/GraphQL.Integration.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | net8 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | 26 | 27 | 28 | 29 | all 30 | runtime; build; native; contentfiles; analyzers; buildtransitive 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/GraphQL.Integration.Tests/Helpers/IntegrationServerTestFixture.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Client.Abstractions.Websocket; 2 | using GraphQL.Client.Http; 3 | using GraphQL.Client.Http.Websocket; 4 | using GraphQL.Client.Serializer.Newtonsoft; 5 | using GraphQL.Client.Serializer.SystemTextJson; 6 | using GraphQL.Client.Tests.Common; 7 | using GraphQL.Client.Tests.Common.Helpers; 8 | 9 | namespace GraphQL.Integration.Tests.Helpers; 10 | 11 | public abstract class IntegrationServerTestFixture 12 | { 13 | public int Port { get; private set; } 14 | 15 | public IWebHost? Server { get; private set; } 16 | 17 | public abstract IGraphQLWebsocketJsonSerializer Serializer { get; } 18 | 19 | public abstract string? WebsocketProtocol { get; } 20 | 21 | public IntegrationServerTestFixture() 22 | { 23 | Port = NetworkHelpers.GetFreeTcpPortNumber(); 24 | } 25 | 26 | public async Task CreateServer() 27 | { 28 | if (Server != null) 29 | return; 30 | Server = await WebHostHelpers.CreateServer(Port).ConfigureAwait(false); 31 | } 32 | 33 | public async Task ShutdownServer() 34 | { 35 | if (Server == null) 36 | return; 37 | 38 | await Server.StopAsync(); 39 | Server.Dispose(); 40 | Server = null; 41 | } 42 | 43 | public GraphQLHttpClient GetStarWarsClient(Action? configure = null) 44 | => GetGraphQLClient(Common.STAR_WARS_ENDPOINT, configure); 45 | 46 | public GraphQLHttpClient GetChatClient(Action? configure = null) 47 | => GetGraphQLClient(Common.CHAT_ENDPOINT, configure); 48 | 49 | private GraphQLHttpClient GetGraphQLClient(string endpoint, Action? configure) => 50 | Serializer == null 51 | ? throw new InvalidOperationException("JSON serializer not configured") 52 | : WebHostHelpers.GetGraphQLClient(Port, endpoint, Serializer, options => 53 | { 54 | configure?.Invoke(options); 55 | options.WebSocketProtocol = WebsocketProtocol; 56 | }); 57 | } 58 | 59 | public class NewtonsoftGraphQLWsServerTestFixture : IntegrationServerTestFixture 60 | { 61 | public override IGraphQLWebsocketJsonSerializer Serializer { get; } = new NewtonsoftJsonSerializer(); 62 | public override string? WebsocketProtocol => WebSocketProtocols.GRAPHQL_WS; 63 | } 64 | 65 | public class SystemTextJsonGraphQLWsServerTestFixture : IntegrationServerTestFixture 66 | { 67 | public override IGraphQLWebsocketJsonSerializer Serializer { get; } = new SystemTextJsonSerializer(); 68 | public override string? WebsocketProtocol => WebSocketProtocols.GRAPHQL_WS; 69 | } 70 | 71 | public class NewtonsoftGraphQLTransportWsServerTestFixture : IntegrationServerTestFixture 72 | { 73 | public override IGraphQLWebsocketJsonSerializer Serializer { get; } = new NewtonsoftJsonSerializer(); 74 | public override string? WebsocketProtocol => WebSocketProtocols.GRAPHQL_TRANSPORT_WS; 75 | } 76 | 77 | public class SystemTextJsonGraphQLTransportWsServerTestFixture : IntegrationServerTestFixture 78 | { 79 | public override IGraphQLWebsocketJsonSerializer Serializer { get; } = new SystemTextJsonSerializer(); 80 | public override string? WebsocketProtocol => WebSocketProtocols.GRAPHQL_TRANSPORT_WS; 81 | } 82 | public class SystemTextJsonAutoNegotiateServerTestFixture : IntegrationServerTestFixture 83 | { 84 | public override IGraphQLWebsocketJsonSerializer Serializer { get; } = new SystemTextJsonSerializer(); 85 | public override string? WebsocketProtocol => WebSocketProtocols.AUTO_NEGOTIATE; 86 | } 87 | -------------------------------------------------------------------------------- /tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Client.Abstractions.Websocket; 2 | using GraphQL.Client.Http; 3 | using GraphQL.Client.Serializer.Newtonsoft; 4 | using IntegrationTestServer; 5 | 6 | namespace GraphQL.Integration.Tests.Helpers; 7 | 8 | public static class WebHostHelpers 9 | { 10 | public static async Task CreateServer(int port) 11 | { 12 | var configBuilder = new ConfigurationBuilder(); 13 | configBuilder.AddInMemoryCollection(); 14 | var config = configBuilder.Build(); 15 | config["server.urls"] = $"http://localhost:{port}"; 16 | 17 | var host = new WebHostBuilder() 18 | .ConfigureLogging((ctx, logging) => logging.AddDebug()) 19 | .UseConfiguration(config) 20 | .UseKestrel() 21 | .UseStartup() 22 | .Build(); 23 | 24 | var tcs = new TaskCompletionSource(); 25 | host.Services.GetService().ApplicationStarted.Register(() => tcs.TrySetResult(true)); 26 | await host.StartAsync(); 27 | await tcs.Task; 28 | return host; 29 | } 30 | 31 | public static GraphQLHttpClient GetGraphQLClient( 32 | int port, 33 | string endpoint, 34 | IGraphQLWebsocketJsonSerializer? serializer = null, 35 | Action? configure = null) 36 | { 37 | var options = new GraphQLHttpClientOptions(); 38 | configure?.Invoke(options); 39 | options.EndPoint = new Uri($"http://localhost:{port}{endpoint}"); 40 | return new GraphQLHttpClient(options, serializer ?? new NewtonsoftJsonSerializer()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/GraphQL.Integration.Tests/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "IIS Express": { 4 | "commandName": "IISExpress", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | } 9 | }, 10 | "GraphQL.Integration.Tests": { 11 | "commandName": "Project", 12 | "launchBrowser": true, 13 | "environmentVariables": { 14 | "ASPNETCORE_ENVIRONMENT": "Development" 15 | }, 16 | "applicationUrl": "http://localhost:51433/" 17 | } 18 | }, 19 | "iisSettings": { 20 | "windowsAuthentication": false, 21 | "anonymousAuthentication": true, 22 | "iisExpress": { 23 | "applicationUrl": "http://localhost:55273/", 24 | "sslPort": 44359 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /tests/GraphQL.Integration.Tests/QueryAndMutationTests/Newtonsoft.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Integration.Tests.Helpers; 2 | using Xunit; 3 | 4 | namespace GraphQL.Integration.Tests.QueryAndMutationTests; 5 | 6 | public class Newtonsoft : Base, IClassFixture 7 | { 8 | public Newtonsoft(NewtonsoftGraphQLWsServerTestFixture fixture) : base(fixture) 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/GraphQL.Integration.Tests/QueryAndMutationTests/SystemTextJson.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Integration.Tests.Helpers; 2 | using Xunit; 3 | 4 | namespace GraphQL.Integration.Tests.QueryAndMutationTests; 5 | 6 | public class SystemTextJson : Base, IClassFixture 7 | { 8 | public SystemTextJson(SystemTextJsonGraphQLWsServerTestFixture fixture) : base(fixture) 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/GraphQL.Integration.Tests/UriExtensionTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using GraphQL.Client.Http; 3 | using Xunit; 4 | 5 | namespace GraphQL.Integration.Tests; 6 | 7 | public class UriExtensionTests 8 | { 9 | [Theory] 10 | [InlineData("http://thats-not-a-websocket-url.net", false)] 11 | [InlineData("https://thats-not-a-websocket-url.net", false)] 12 | [InlineData("ftp://thats-not-a-websocket-url.net", false)] 13 | [InlineData("ws://that-is-a-websocket-url.net", true)] 14 | [InlineData("wss://that-is-a-websocket-url.net", true)] 15 | [InlineData("WS://that-is-a-websocket-url.net", true)] 16 | [InlineData("WSS://that-is-a-websocket-url.net", true)] 17 | public void HasWebSocketSchemaTest(string url, bool result) 18 | { 19 | new Uri(url).HasWebSocketScheme().Should().Be(result); 20 | } 21 | 22 | [Theory] 23 | [InlineData("http://this-url-can-be-converted.net", true, "ws://this-url-can-be-converted.net")] 24 | [InlineData("https://this-url-can-be-converted.net", true, "wss://this-url-can-be-converted.net")] 25 | [InlineData("HTTP://this-url-can-be-converted.net", true, "ws://this-url-can-be-converted.net")] 26 | [InlineData("HTTPS://this-url-can-be-converted.net", true, "wss://this-url-can-be-converted.net")] 27 | [InlineData("ws://this-url-can-be-converted.net", true, "ws://this-url-can-be-converted.net")] 28 | [InlineData("wss://this-url-can-be-converted.net", true, "wss://this-url-can-be-converted.net")] 29 | [InlineData("https://this-url-can-be-converted.net/and/all/elements/?are#preserved", true, "wss://this-url-can-be-converted.net/and/all/elements/?are#preserved")] 30 | [InlineData("ftp://this-url-cannot-be-converted.net", false, null)] 31 | // AppSync example 32 | [InlineData("wss://example1234567890000.appsync-realtime-api.us-west-2.amazonaws.com/graphql?header=123456789ABCDEF&payload=e30=", true, "wss://example1234567890000.appsync-realtime-api.us-west-2.amazonaws.com/graphql?header=123456789ABCDEF&payload=e30=")] 33 | public void GetWebSocketUriTest(string input, bool canConvert, string? result) 34 | { 35 | var inputUri = new Uri(input); 36 | if (canConvert) 37 | { 38 | inputUri.GetWebSocketUri().Should().BeEquivalentTo(new Uri(result!)); 39 | } 40 | else 41 | { 42 | inputUri.Invoking(uri => uri.GetWebSocketUri()).Should().Throw(); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/GraphQL.Integration.Tests/UserAgentHeaderTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using FluentAssertions; 3 | using GraphQL.Client.Abstractions; 4 | using GraphQL.Client.Http; 5 | using GraphQL.Integration.Tests.Helpers; 6 | using Xunit; 7 | 8 | namespace GraphQL.Integration.Tests; 9 | 10 | public class UserAgentHeaderTests : IAsyncLifetime, IClassFixture 11 | { 12 | private readonly IntegrationServerTestFixture Fixture; 13 | private GraphQLHttpClient? ChatClient; 14 | 15 | public UserAgentHeaderTests(SystemTextJsonAutoNegotiateServerTestFixture fixture) 16 | { 17 | Fixture = fixture; 18 | } 19 | 20 | public async Task InitializeAsync() => await Fixture.CreateServer(); 21 | 22 | public Task DisposeAsync() 23 | { 24 | ChatClient?.Dispose(); 25 | return Task.CompletedTask; 26 | } 27 | 28 | [Fact] 29 | public async void Can_set_custom_user_agent() 30 | { 31 | const string userAgent = "CustomUserAgent"; 32 | ChatClient = Fixture.GetChatClient(options => options.DefaultUserAgentRequestHeader = ProductInfoHeaderValue.Parse(userAgent)); 33 | 34 | var graphQLRequest = new GraphQLRequest("query clientUserAgent { clientUserAgent }"); 35 | var response = await ChatClient.SendQueryAsync(graphQLRequest, () => new { clientUserAgent = string.Empty }); 36 | 37 | response.Errors.Should().BeNull(); 38 | response.Data.clientUserAgent.Should().Be(userAgent); 39 | } 40 | 41 | [Fact] 42 | public async void Default_user_agent_is_set_as_expected() 43 | { 44 | string? expectedUserAgent = new ProductInfoHeaderValue( 45 | typeof(GraphQLHttpClient).Assembly.GetName().Name, 46 | typeof(GraphQLHttpClient).Assembly.GetName().Version.ToString()).ToString(); 47 | 48 | ChatClient = Fixture.GetChatClient(); 49 | 50 | var graphQLRequest = new GraphQLRequest("query clientUserAgent { clientUserAgent }"); 51 | var response = await ChatClient.SendQueryAsync(graphQLRequest, () => new { clientUserAgent = string.Empty }); 52 | 53 | response.Errors.Should().BeNull(); 54 | response.Data.clientUserAgent.Should().Be(expectedUserAgent); 55 | } 56 | 57 | [Fact] 58 | public async void No_Default_user_agent_if_set_to_null() 59 | { 60 | ChatClient = Fixture.GetChatClient(options => options.DefaultUserAgentRequestHeader = null); 61 | 62 | var graphQLRequest = new GraphQLRequest("query clientUserAgent { clientUserAgent }"); 63 | var response = await ChatClient.SendQueryAsync(graphQLRequest, () => new { clientUserAgent = string.Empty }); 64 | 65 | response.Errors.Should().HaveCount(1); 66 | response.Errors[0].Message.Should().Be("user agent header not set"); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/GraphQL.Integration.Tests/WebsocketTests/NewtonsoftGraphQLTransportWs.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Integration.Tests.Helpers; 2 | using Xunit; 3 | using Xunit.Abstractions; 4 | 5 | namespace GraphQL.Integration.Tests.WebsocketTests; 6 | 7 | public class NewtonsoftGraphQLTransportWs : Base, IClassFixture 8 | { 9 | public NewtonsoftGraphQLTransportWs(ITestOutputHelper output, NewtonsoftGraphQLTransportWsServerTestFixture fixture) : base(output, fixture) 10 | { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/GraphQL.Integration.Tests/WebsocketTests/NewtonsoftGraphQLWs.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Integration.Tests.Helpers; 2 | using Xunit; 3 | using Xunit.Abstractions; 4 | 5 | namespace GraphQL.Integration.Tests.WebsocketTests; 6 | 7 | public class NewtonsoftGraphQLWs : Base, IClassFixture 8 | { 9 | public NewtonsoftGraphQLWs(ITestOutputHelper output, NewtonsoftGraphQLWsServerTestFixture fixture) : base(output, fixture) 10 | { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/GraphQL.Integration.Tests/WebsocketTests/SystemTextJsonAutoNegotiate.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using GraphQL.Client.Http.Websocket; 3 | using GraphQL.Integration.Tests.Helpers; 4 | using Xunit; 5 | using Xunit.Abstractions; 6 | 7 | namespace GraphQL.Integration.Tests.WebsocketTests; 8 | 9 | public class SystemTextJsonAutoNegotiate : Base, IClassFixture 10 | { 11 | public SystemTextJsonAutoNegotiate(ITestOutputHelper output, SystemTextJsonAutoNegotiateServerTestFixture fixture) : base(output, fixture) 12 | { 13 | } 14 | 15 | [Fact] 16 | public async Task WebSocketProtocolIsAutoNegotiated() 17 | { 18 | await ChatClient.InitializeWebsocketConnection(); 19 | ChatClient.WebSocketSubProtocol.Should().Be(WebSocketProtocols.GRAPHQL_TRANSPORT_WS); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/GraphQL.Integration.Tests/WebsocketTests/SystemTextJsonGraphQLTransportWs.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using FluentAssertions.Reactive; 3 | using GraphQL.Integration.Tests.Helpers; 4 | using Xunit; 5 | using Xunit.Abstractions; 6 | 7 | namespace GraphQL.Integration.Tests.WebsocketTests; 8 | 9 | public class SystemTextJsonGraphQLTransportWs : Base, IClassFixture 10 | { 11 | public SystemTextJsonGraphQLTransportWs(ITestOutputHelper output, SystemTextJsonGraphQLTransportWsServerTestFixture fixture) : base(output, fixture) 12 | { 13 | } 14 | 15 | [Fact] 16 | public async void Sending_a_pong_message_should_not_throw() 17 | { 18 | await ChatClient.InitializeWebsocketConnection(); 19 | 20 | await ChatClient.Awaiting(client => client.SendPongAsync(null)).Should().NotThrowAsync(); 21 | await ChatClient.Awaiting(client => client.SendPongAsync("some payload")).Should().NotThrowAsync(); 22 | } 23 | 24 | [Fact] 25 | public async void Sending_a_ping_message_should_result_in_a_pong_message_from_the_server() 26 | { 27 | await ChatClient.InitializeWebsocketConnection(); 28 | 29 | using var pongObserver = ChatClient.PongStream.Observe(); 30 | 31 | await ChatClient.Awaiting(client => client.SendPingAsync(null)).Should().NotThrowAsync(); 32 | 33 | await pongObserver.Should().PushAsync(1, TimeSpan.FromSeconds(1), "because the server was pinged by the client"); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/GraphQL.Integration.Tests/WebsocketTests/SystemTextJsonGraphQLWs.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using GraphQL.Integration.Tests.Helpers; 3 | using Xunit; 4 | using Xunit.Abstractions; 5 | 6 | namespace GraphQL.Integration.Tests.WebsocketTests; 7 | 8 | public class SystemTextJsonGraphQLWs : Base, IClassFixture 9 | { 10 | public SystemTextJsonGraphQLWs(ITestOutputHelper output, SystemTextJsonGraphQLWsServerTestFixture fixture) : base(output, fixture) 11 | { 12 | } 13 | 14 | [Fact] 15 | public async void Sending_a_ping_message_should_throw_not_supported_exception() 16 | { 17 | await ChatClient.InitializeWebsocketConnection(); 18 | 19 | await ChatClient.Awaiting(client => client.SendPingAsync(null)) 20 | .Should().ThrowAsync(); 21 | } 22 | 23 | [Fact] 24 | public async void Sending_a_pong_message_should_throw_not_supported_exception() 25 | { 26 | await ChatClient.InitializeWebsocketConnection(); 27 | 28 | await ChatClient.Awaiting(client => client.SendPongAsync(null)) 29 | .Should().ThrowAsync(); 30 | } 31 | 32 | [Fact] 33 | public async void Subscribing_to_the_pong_stream_should_throw_not_supported_exception() 34 | { 35 | await ChatClient.InitializeWebsocketConnection(); 36 | 37 | ChatClient.Invoking(client => client.PongStream.Subscribe()) 38 | .Should().Throw(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/GraphQL.Primitives.Tests/GraphQL.Primitives.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | net8 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | all 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/GraphQL.Primitives.Tests/GraphQLLocationTest.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace GraphQL.Primitives.Tests; 4 | 5 | public class GraphQLLocationTest 6 | { 7 | [Fact] 8 | public void ConstructorFact() 9 | { 10 | var graphQLLocation = new GraphQLLocation { Column = 1, Line = 2 }; 11 | Assert.Equal(1U, graphQLLocation.Column); 12 | Assert.Equal(2U, graphQLLocation.Line); 13 | } 14 | 15 | [Fact] 16 | public void Equality1Fact() 17 | { 18 | var graphQLLocation = new GraphQLLocation { Column = 1, Line = 2 }; 19 | Assert.Equal(graphQLLocation, graphQLLocation); 20 | } 21 | 22 | [Fact] 23 | public void Equality2Fact() 24 | { 25 | var graphQLLocation1 = new GraphQLLocation { Column = 1, Line = 2 }; 26 | var graphQLLocation2 = new GraphQLLocation { Column = 1, Line = 2 }; 27 | Assert.Equal(graphQLLocation1, graphQLLocation2); 28 | } 29 | 30 | [Fact] 31 | public void EqualityOperatorFact() 32 | { 33 | var graphQLLocation1 = new GraphQLLocation { Column = 1, Line = 2 }; 34 | var graphQLLocation2 = new GraphQLLocation { Column = 1, Line = 2 }; 35 | Assert.True(graphQLLocation1 == graphQLLocation2); 36 | } 37 | 38 | [Fact] 39 | public void InEqualityFact() 40 | { 41 | var graphQLLocation1 = new GraphQLLocation { Column = 1, Line = 2 }; 42 | var graphQLLocation2 = new GraphQLLocation { Column = 2, Line = 1 }; 43 | Assert.NotEqual(graphQLLocation1, graphQLLocation2); 44 | } 45 | 46 | [Fact] 47 | public void InEqualityOperatorFact() 48 | { 49 | var graphQLLocation1 = new GraphQLLocation { Column = 1, Line = 2 }; 50 | var graphQLLocation2 = new GraphQLLocation { Column = 2, Line = 1 }; 51 | Assert.True(graphQLLocation1 != graphQLLocation2); 52 | } 53 | 54 | [Fact] 55 | public void GetHashCodeFact() 56 | { 57 | var graphQLLocation1 = new GraphQLLocation { Column = 1, Line = 2 }; 58 | var graphQLLocation2 = new GraphQLLocation { Column = 1, Line = 2 }; 59 | Assert.True(graphQLLocation1.GetHashCode() == graphQLLocation2.GetHashCode()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/GraphQL.Primitives.Tests/GraphQLResponseTest.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace GraphQL.Primitives.Tests; 4 | 5 | public class GraphQLResponseTest 6 | { 7 | [Fact] 8 | public void Constructor1Fact() 9 | { 10 | var graphQLResponse = new GraphQLResponse(); 11 | Assert.Null(graphQLResponse.Data); 12 | Assert.Null(graphQLResponse.Errors); 13 | } 14 | 15 | [Fact] 16 | public void Constructor2Fact() 17 | { 18 | var graphQLResponse = new GraphQLResponse 19 | { 20 | Data = new { a = 1 }, 21 | Errors = new[] { new GraphQLError { Message = "message" } } 22 | }; 23 | Assert.NotNull(graphQLResponse.Data); 24 | Assert.NotNull(graphQLResponse.Errors); 25 | } 26 | 27 | [Fact] 28 | public void Equality1Fact() 29 | { 30 | var graphQLResponse = new GraphQLResponse(); 31 | Assert.Equal(graphQLResponse, graphQLResponse); 32 | } 33 | 34 | [Fact] 35 | public void Equality2Fact() 36 | { 37 | var graphQLResponse1 = new GraphQLResponse(); 38 | var graphQLResponse2 = new GraphQLResponse(); 39 | Assert.Equal(graphQLResponse1, graphQLResponse2); 40 | } 41 | 42 | [Fact] 43 | public void Equality3Fact() 44 | { 45 | var graphQLResponse1 = new GraphQLResponse 46 | { 47 | Data = new { a = 1 }, 48 | Errors = new[] { new GraphQLError { Message = "message" } } 49 | }; 50 | var graphQLResponse2 = new GraphQLResponse 51 | { 52 | Data = new { a = 1 }, 53 | Errors = new[] { new GraphQLError { Message = "message" } } 54 | }; 55 | Assert.Equal(graphQLResponse1, graphQLResponse2); 56 | } 57 | 58 | [Fact] 59 | public void EqualityOperatorFact() 60 | { 61 | var graphQLResponse1 = new GraphQLResponse(); 62 | var graphQLResponse2 = new GraphQLResponse(); 63 | Assert.True(graphQLResponse1 == graphQLResponse2); 64 | } 65 | 66 | [Fact] 67 | public void InEqualityFact() 68 | { 69 | var graphQLResponse1 = new GraphQLResponse 70 | { 71 | Data = new { a = 1 }, 72 | Errors = new[] { new GraphQLError { Message = "message" } } 73 | }; 74 | var graphQLResponse2 = new GraphQLResponse 75 | { 76 | Data = new { a = 2 }, 77 | Errors = new[] { new GraphQLError { Message = "message" } } 78 | }; 79 | Assert.NotEqual(graphQLResponse1, graphQLResponse2); 80 | } 81 | 82 | [Fact] 83 | public void InEqualityOperatorFact() 84 | { 85 | var graphQLResponse1 = new GraphQLResponse 86 | { 87 | Data = new { a = 1 }, 88 | Errors = new[] { new GraphQLError { Message = "message" } } 89 | }; 90 | var graphQLResponse2 = new GraphQLResponse 91 | { 92 | Data = new { a = 2 }, 93 | Errors = new[] { new GraphQLError { Message = "message" } } 94 | }; 95 | Assert.True(graphQLResponse1 != graphQLResponse2); 96 | } 97 | 98 | [Fact] 99 | public void GetHashCodeFact() 100 | { 101 | var graphQLResponse1 = new GraphQLResponse 102 | { 103 | Data = new { a = 1 }, 104 | Errors = new[] { new GraphQLError { Message = "message" } } 105 | }; 106 | var graphQLResponse2 = new GraphQLResponse 107 | { 108 | Data = new { a = 1 }, 109 | Errors = new[] { new GraphQLError { Message = "message" } } 110 | }; 111 | Assert.True(graphQLResponse1.GetHashCode() == graphQLResponse2.GetHashCode()); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /tests/GraphQL.Primitives.Tests/JsonSerializationTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using FluentAssertions; 3 | using Xunit; 4 | 5 | namespace GraphQL.Primitives.Tests; 6 | 7 | public class JsonSerializationTests 8 | { 9 | [Fact] 10 | public void WebSocketResponseDeserialization() 11 | { 12 | var testObject = new ExtendedTestObject { Id = "test", OtherData = "this is some other stuff" }; 13 | var json = JsonSerializer.Serialize(testObject); 14 | var deserialized = JsonSerializer.Deserialize(json); 15 | deserialized.Id.Should().Be("test"); 16 | var dict = JsonSerializer.Deserialize>(json); 17 | var childObject = (JsonElement)dict["ChildObject"]; 18 | childObject.GetProperty("Id").GetString().Should().Be(testObject.ChildObject.Id); 19 | } 20 | 21 | public class TestObject 22 | { 23 | public string Id { get; set; } 24 | } 25 | 26 | public class ExtendedTestObject : TestObject 27 | { 28 | public string OtherData { get; set; } 29 | 30 | public TestObject ChildObject { get; set; } = new TestObject { Id = "1337" }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/GraphQL.Server.Test/GraphQL.Server.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/GraphQL.Server.Test/GraphQL/Models/Repository.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | 3 | namespace GraphQL.Server.Test.GraphQL.Models; 4 | 5 | public class Repository 6 | { 7 | public int DatabaseId { get; set; } 8 | 9 | public string? Id { get; set; } 10 | 11 | public string? Name { get; set; } 12 | 13 | public object? Owner { get; set; } 14 | 15 | public Uri? Url { get; set; } 16 | } 17 | 18 | public class RepositoryGraphType : ObjectGraphType 19 | { 20 | public RepositoryGraphType() 21 | { 22 | Name = nameof(Repository); 23 | Field(expression => expression.DatabaseId); 24 | Field>("id"); 25 | Field(expression => expression.Name); 26 | //this.Field(expression => expression.Owner); 27 | Field>("url"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/GraphQL.Server.Test/GraphQL/Storage.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Server.Test.GraphQL.Models; 2 | 3 | namespace GraphQL.Server.Test.GraphQL; 4 | 5 | public static class Storage 6 | { 7 | public static IQueryable Repositories { get; } = new List() 8 | .Append(new Repository 9 | { 10 | DatabaseId = 113196300, 11 | Id = "MDEwOlJlcG9zaXRvcnkxMTMxOTYzMDA=", 12 | Name = "graphql-client", 13 | Owner = null, 14 | Url = new Uri("https://github.com/graphql-dotnet/graphql-client") 15 | }) 16 | .AsQueryable(); 17 | } 18 | -------------------------------------------------------------------------------- /tests/GraphQL.Server.Test/GraphQL/TestMutation.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | 3 | namespace GraphQL.Server.Test.GraphQL; 4 | 5 | public class TestMutation : ObjectGraphType 6 | { 7 | public TestMutation() 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/GraphQL.Server.Test/GraphQL/TestQuery.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Server.Test.GraphQL.Models; 2 | using GraphQL.Types; 3 | 4 | namespace GraphQL.Server.Test.GraphQL; 5 | 6 | public class TestQuery : ObjectGraphType 7 | { 8 | public TestQuery() 9 | { 10 | Field("repository") 11 | .Argument>("owner") 12 | .Argument>("name") 13 | .Resolve(context => 14 | { 15 | var owner = context.GetArgument("owner"); 16 | var name = context.GetArgument("name"); 17 | return Storage.Repositories.FirstOrDefault(predicate => predicate.Name == name); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/GraphQL.Server.Test/GraphQL/TestSchema.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | 3 | namespace GraphQL.Server.Test.GraphQL; 4 | 5 | public class TestSchema : Schema 6 | { 7 | public TestSchema() 8 | { 9 | Query = new TestQuery(); 10 | //this.Mutation = new TestMutation(); 11 | //this.Subscription = new TestSubscription(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/GraphQL.Server.Test/GraphQL/TestSubscription.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | 3 | namespace GraphQL.Server.Test.GraphQL; 4 | 5 | public class TestSubscription : ObjectGraphType 6 | { 7 | public TestSubscription() 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/GraphQL.Server.Test/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | 3 | namespace GraphQL.Server.Test; 4 | 5 | public static class Program 6 | { 7 | public static async Task Main(string[] args) => 8 | await CreateHostBuilder(args).Build().RunAsync(); 9 | 10 | public static IWebHostBuilder CreateHostBuilder(string[] args = null) => 11 | WebHost.CreateDefaultBuilder(args) 12 | .UseKestrel(options => options.AllowSynchronousIO = true) 13 | .UseStartup(); 14 | } 15 | -------------------------------------------------------------------------------- /tests/GraphQL.Server.Test/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings", 3 | "profiles": { 4 | "GraphQL.Server.Test": { 5 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 6 | "commandName": "Project", 7 | "environmentVariables": { 8 | "ASPNETCORE_ENVIRONMENT": "Development" 9 | }, 10 | "launchBrowser": true 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/GraphQL.Server.Test/Startup.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Server.Test.GraphQL; 2 | 3 | namespace GraphQL.Server.Test; 4 | 5 | public class Startup 6 | { 7 | public void Configure(IApplicationBuilder app) 8 | { 9 | var webHostEnvironment = app.ApplicationServices.GetRequiredService(); 10 | if (webHostEnvironment.IsDevelopment()) 11 | { 12 | app.UseDeveloperExceptionPage(); 13 | } 14 | app.UseHttpsRedirection(); 15 | 16 | app.UseWebSockets(); 17 | app.UseGraphQL(); 18 | app.UseGraphQLGraphiQL(); 19 | } 20 | 21 | public void ConfigureServices(IServiceCollection services) 22 | { 23 | services.AddGraphQL(builder => builder 24 | .AddSchema() 25 | .AddSystemTextJson() 26 | .UseApolloTracing(enableMetrics: true) 27 | .AddErrorInfoProvider(opt => opt.ExposeExceptionDetails = true) 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/GraphQL.Server.Test/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/appsettings", 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/GraphQL.Server.Test/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/appsettings", 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /tests/GraphQL.Server.Test/libman.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "defaultProvider": "cdnjs", 4 | "libraries": [] 5 | } -------------------------------------------------------------------------------- /tests/IntegrationTestServer/IntegrationTestServer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8 5 | IntegrationTestServer.Program 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/IntegrationTestServer/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | 3 | namespace IntegrationTestServer; 4 | 5 | public static class Program 6 | { 7 | public static void Main(string[] args) => CreateWebHostBuilder(args).Build().Run(); 8 | 9 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 10 | WebHost.CreateDefaultBuilder(args) 11 | .UseStartup() 12 | .ConfigureLogging((_, logging) => logging.SetMinimumLevel(LogLevel.Debug)); 13 | } 14 | -------------------------------------------------------------------------------- /tests/IntegrationTestServer/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:55069/ui/graphiql/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "IntegrationTestServer": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "launchUrl": "ui/graphiql", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | }, 25 | "applicationUrl": "http://localhost:5005" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/IntegrationTestServer/Startup.cs: -------------------------------------------------------------------------------- 1 | using GraphQL; 2 | using GraphQL.Client.Tests.Common; 3 | using GraphQL.Client.Tests.Common.Chat.Schema; 4 | using GraphQL.Client.Tests.Common.StarWars; 5 | using GraphQL.Server.Ui.Altair; 6 | using GraphQL.Server.Ui.GraphiQL; 7 | using Microsoft.AspNetCore.Server.Kestrel.Core; 8 | 9 | namespace IntegrationTestServer; 10 | 11 | public class Startup 12 | { 13 | public Startup(IConfiguration configuration, IWebHostEnvironment environment) 14 | { 15 | Configuration = configuration; 16 | Environment = environment; 17 | } 18 | 19 | public IConfiguration Configuration { get; } 20 | 21 | public IWebHostEnvironment Environment { get; } 22 | 23 | // This method gets called by the runtime. Use this method to add services to the container. 24 | // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 25 | public void ConfigureServices(IServiceCollection services) 26 | { 27 | services.Configure(options => options.AllowSynchronousIO = true); 28 | services.AddHttpContextAccessor(); 29 | services.AddChatSchema(); 30 | services.AddStarWarsSchema(); 31 | services.AddGraphQL(builder => builder 32 | .UseApolloTracing(enableMetrics: true) 33 | .ConfigureExecutionOptions(opt => opt.UnhandledExceptionDelegate = ctx => 34 | { 35 | var logger = ctx.Context.RequestServices.GetRequiredService>(); 36 | logger.LogError("{Error} occurred", ctx.OriginalException.Message); 37 | return Task.CompletedTask; 38 | }) 39 | .AddErrorInfoProvider(opt => opt.ExposeExceptionDetails = Environment.IsDevelopment()) 40 | .AddSystemTextJson() 41 | .UseAutomaticPersistedQueries(options => options.TrackLinkedCacheEntries = true) 42 | .AddGraphTypes(typeof(ChatSchema).Assembly)); 43 | } 44 | 45 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 46 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 47 | { 48 | if (env.IsDevelopment()) 49 | { 50 | app.UseDeveloperExceptionPage(); 51 | } 52 | app.UseWebSockets(); 53 | 54 | app.UseGraphQL(Common.CHAT_ENDPOINT); 55 | app.UseGraphQL(Common.STAR_WARS_ENDPOINT); 56 | 57 | app.UseGraphQLGraphiQL(options: new GraphiQLOptions { GraphQLEndPoint = Common.STAR_WARS_ENDPOINT }); 58 | app.UseGraphQLAltair(options: new AltairOptions { GraphQLEndPoint = Common.CHAT_ENDPOINT }); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/tests.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | $(NoWarn);NU1701;IDE1006 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | all 15 | runtime;build;native;contentfiles;analyzers;buildtransitive 16 | 17 | 18 | 19 | 20 | --------------------------------------------------------------------------------