diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
deleted file mode 100644
index 18cff76a..00000000
--- a/.github/FUNDING.yml
+++ /dev/null
@@ -1 +0,0 @@
-github: deinok
diff --git a/.github/workflows/branches.yml b/.github/workflows/branches.yml
new file mode 100644
index 00000000..a896d523
--- /dev/null
+++ b/.github/workflows/branches.yml
@@ -0,0 +1,80 @@
+name: Branch workflow
+on:
+ push:
+ branches-ignore:
+ - develop
+ - 'release/**'
+ - 'releases/**'
+jobs:
+ generateVersionInfo:
+ name: GenerateVersionInfo
+ runs-on: windows-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
+ - name: Setup dotnet environment
+ uses: actions/setup-dotnet@master
+ with:
+ dotnet-version: '3.1.100'
+ - name: Restore dotnet tools
+ run: dotnet tool restore
+ - name: Fetch complete repository
+ run: git fetch
+ - name: Generate version info from git history
+ run: dotnet dotnet-gitversion /output json | Out-File gitversion.json; Get-Content gitversion.json
+ - name: Upload version info file
+ uses: actions/upload-artifact@v1
+ with:
+ name: gitversion
+ path: gitversion.json
+
+ build:
+ name: Build
+ needs: generateVersionInfo
+ runs-on: windows-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - name: Setup dotnet environment
+ uses: actions/setup-dotnet@master
+ with:
+ dotnet-version: '3.1.100'
+ - name: Download version info file
+ uses: actions/download-artifact@v1
+ with:
+ name: gitversion
+ path: ./
+ - name: Inject version info into environment
+ run: Get-Content gitversion.json | ConvertFrom-Json | ForEach-Object { foreach ($item in $_.PSObject.properties) { "::set-env name=GitVersion_$($item.Name)::$($item.Value)" } }; $env:GitVersion_SemVer
+ - name: Build solution
+ run: dotnet build -c Release
+ - name: Create NuGet packages
+ run: dotnet pack -c Release --no-build -o nupkg
+ - name: Upload nuget packages
+ uses: actions/upload-artifact@v1
+ with:
+ name: nupkg
+ path: nupkg
+
+ test:
+ name: Test
+ needs: [build, generateVersionInfo]
+ runs-on: windows-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - name: Setup dotnet environment
+ uses: actions/setup-dotnet@master
+ with:
+ dotnet-version: '3.1.100'
+ - name: Download version info file
+ uses: actions/download-artifact@v1
+ with:
+ name: gitversion
+ path: ./
+ - name: Inject version info into environment
+ run: Get-Content gitversion.json | ConvertFrom-Json | ForEach-Object { foreach ($item in $_.PSObject.properties) { "::set-env name=GitVersion_$($item.Name)::$($item.Value)" } }
+ - name: Run tests
+ run: dotnet test -c Release -p:ParallelizeTestCollections=false
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 00000000..2ed05127
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,103 @@
+name: Main workflow
+on:
+ push:
+ branches:
+ - develop
+ - 'release/**'
+ - 'releases/**'
+ tags:
+ - v*
+ - V*
+jobs:
+ generateVersionInfo:
+ name: GenerateVersionInfo
+ runs-on: windows-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
+ - name: Setup dotnet environment
+ uses: actions/setup-dotnet@master
+ with:
+ dotnet-version: '3.1.100'
+ - name: Restore dotnet tools
+ run: dotnet tool restore
+ - name: Fetch complete repository
+ run: git fetch
+ - name: Generate version info from git history
+ run: dotnet dotnet-gitversion /output json | Out-File gitversion.json; Get-Content gitversion.json
+ env:
+ IGNORE_NORMALISATION_GIT_HEAD_MOVE: 1
+ - name: Upload version info file
+ uses: actions/upload-artifact@v1
+ with:
+ name: gitversion
+ path: gitversion.json
+
+ build:
+ name: Build
+ needs: generateVersionInfo
+ runs-on: windows-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - name: Setup dotnet environment
+ uses: actions/setup-dotnet@master
+ with:
+ dotnet-version: '3.1.100'
+ - name: Download version info file
+ uses: actions/download-artifact@v1
+ with:
+ name: gitversion
+ path: ./
+ - name: Inject version info into environment
+ run: Get-Content gitversion.json | ConvertFrom-Json | ForEach-Object { foreach ($item in $_.PSObject.properties) { "::set-env name=GitVersion_$($item.Name)::$($item.Value)" } }; $env:GitVersion_SemVer
+ - name: Build solution
+ run: dotnet build -c Release
+ - name: Create NuGet packages
+ run: dotnet pack -c Release --no-build -o nupkg
+ - name: Upload nuget packages
+ uses: actions/upload-artifact@v1
+ with:
+ name: nupkg
+ path: nupkg
+
+ test:
+ name: Test
+ needs: [build, generateVersionInfo]
+ runs-on: windows-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - name: Setup dotnet environment
+ uses: actions/setup-dotnet@master
+ with:
+ dotnet-version: '3.1.100'
+ - name: Download version info file
+ uses: actions/download-artifact@v1
+ with:
+ name: gitversion
+ path: ./
+ - name: Inject version info into environment
+ run: Get-Content gitversion.json | ConvertFrom-Json | ForEach-Object { foreach ($item in $_.PSObject.properties) { "::set-env name=GitVersion_$($item.Name)::$($item.Value)" } }
+ - name: Run tests
+ run: dotnet test -c Release -p:ParallelizeTestCollections=false
+
+ publish:
+ name: Publish
+ needs: [test]
+ runs-on: windows-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - name: Download nuget packages
+ uses: actions/download-artifact@v1
+ with:
+ name: nupkg
+ - name: Setup Nuget.exe
+ uses: warrenbuckley/Setup-Nuget@v1
+ - name: Configure package source
+ run: nuget sources Add -Name "GPR" -Source https://nuget.pkg.github.com/graphql-dotnet/index.json -UserName graphql-dotnet -Password ${{secrets.GITHUB_TOKEN}}
+ - name: push packages
+ run: nuget push .\nupkg\*.nupkg -SkipDuplicate -Source "GPR"
diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml
deleted file mode 100644
index 268c5532..00000000
--- a/.github/workflows/workflow.yml
+++ /dev/null
@@ -1,42 +0,0 @@
-name: Main workflow
-on: [push]
-jobs:
- build:
- name: Build
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@master
- - uses: actions/setup-dotnet@master
- with:
- dotnet-version: '3.1.100'
- - run: dotnet tool restore
- - run: dotnet format --check --dry-run
- - run: dotnet restore
- - run: dotnet build
- - run: dotnet pack
- test:
- name: Test
- needs: build
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@master
- - uses: actions/setup-dotnet@master
- with:
- dotnet-version: '3.1.100'
- - run: dotnet restore
- - run: dotnet build
- - run: dotnet test
- publish:
- name: Publish
- needs: test
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@master
- - uses: actions/setup-dotnet@master
- with:
- dotnet-version: '3.1.100'
- - run: dotnet restore
- - run: dotnet build --configuration Release
- - run: dotnet pack --configuration Release
- - run: dotnet nuget push ./src/GraphQL.Client/bin/Release/GraphQL.Client.2.0.0-alpha.4.nupkg --api-key $GITHUB_TOKEN --source https://nuget.pkg.github.com/graphql-dotnet/graphql-client/index.json
- - run: dotnet nuget push ./src/GraphQL.Client.Http/bin/Release/GraphQL.Client.Http.2.0.0-alpha.4.nupkg --api-key $GITHUB_TOKEN --source https://nuget.pkg.github.com/graphql-dotnet/graphql-client/index.json
diff --git a/GitVersion.yml b/GitVersion.yml
new file mode 100644
index 00000000..4b1b821f
--- /dev/null
+++ b/GitVersion.yml
@@ -0,0 +1,3 @@
+branches:
+ release:
+ mode: ContinuousDeployment
diff --git a/GraphQL.Client.sln b/GraphQL.Client.sln
index 203aae32..1b6cc421 100644
--- a/GraphQL.Client.sln
+++ b/GraphQL.Client.sln
@@ -8,16 +8,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{47C98B55-08F
src\src.props = src\src.props
EndProjectSection
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client", "src\GraphQL.Client\GraphQL.Client.csproj", "{42BEFACE-39F9-4FE4-B725-15CD2B16292E}"
-EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{63F75859-4698-4EDE-8B70-4ACBB8BC425A}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.gitignore = .gitignore
+ dotnet-tools.json = dotnet-tools.json
LICENSE.txt = LICENSE.txt
README.md = README.md
root.props = root.props
- dotnet-tools.json = dotnet-tools.json
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0B0EDB0F-FF67-4B78-A8DB-B5C23E1FEE8C}"
@@ -25,8 +23,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0B0EDB0F
tests\tests.props = tests\tests.props
EndProjectSection
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client.Tests", "tests\GraphQL.Client.Tests\GraphQL.Client.Tests.csproj", "{FEDAE425-B505-4DD6-98ED-3F8593358FC8}"
-EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{6326E0E2-3F48-4BAF-80D3-47AED5EB647C}"
ProjectSection(SolutionItems) = preProject
assets\logo.64x64.png = assets\logo.64x64.png
@@ -42,20 +38,25 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{C421
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{05CAF9B2-981E-40C0-AE31-5FA56E351F12}"
ProjectSection(SolutionItems) = preProject
- .github\workflows\workflow.yml = .github\workflows\workflow.yml
+ .github\workflows\branches.yml = .github\workflows\branches.yml
+ .github\workflows\main.yml = .github\workflows\main.yml
EndProjectSection
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client.Http", "src\GraphQL.Client.Http\GraphQL.Client.Http.csproj", "{FA10201B-AE2A-4BFC-8E8F-6F944680974D}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client.Http.Tests", "tests\GraphQL.Client.Http.Tests\GraphQL.Client.Http.Tests.csproj", "{8F5BBBDA-B3DD-458B-B97F-F67531D323E0}"
-EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{D61415CA-D822-43DD-9AE7-993B8B60E855}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client.Http.Examples", "examples\GraphQL.Client.Http.Examples\GraphQL.Client.Http.Examples.csproj", "{95D78D57-3232-491D-BAD6-F373D76EA34D}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL.Primitives", "src\GraphQL.Primitives\GraphQL.Primitives.csproj", "{87FC440E-6A4D-47D8-9EB2-416FC31CC4A6}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Primitives", "src\GraphQL.Primitives\GraphQL.Primitives.csproj", "{87FC440E-6A4D-47D8-9EB2-416FC31CC4A6}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Primitives.Tests", "tests\GraphQL.Primitives.Tests\GraphQL.Primitives.Tests.csproj", "{C212983F-67DB-44EB-BFB0-5DA75A86DF55}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTestServer", "tests\IntegrationTestServer\IntegrationTestServer.csproj", "{92107DF5-73DF-4371-8EB1-6734FED704AD}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Integration.Tests", "tests\GraphQL.Integration.Tests\GraphQL.Integration.Tests.csproj", "{C68C26EB-7659-402A-93D1-E6E248DA5427}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client.Abstractions", "src\GraphQL.Client.Abstractions\GraphQL.Client.Abstractions.csproj", "{76E622F6-7CDD-4B1F-AD06-FFABF37C55E5}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL.Primitives.Tests", "tests\GraphQL.Primitives.Tests\GraphQL.Primitives.Tests.csproj", "{C212983F-67DB-44EB-BFB0-5DA75A86DF55}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Client", "src\GraphQL.Client\GraphQL.Client.csproj", "{ED3541C9-D2B2-4D06-A464-38E404A3919A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -63,26 +64,10 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {42BEFACE-39F9-4FE4-B725-15CD2B16292E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {42BEFACE-39F9-4FE4-B725-15CD2B16292E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {42BEFACE-39F9-4FE4-B725-15CD2B16292E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {42BEFACE-39F9-4FE4-B725-15CD2B16292E}.Release|Any CPU.Build.0 = Release|Any CPU
- {FEDAE425-B505-4DD6-98ED-3F8593358FC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {FEDAE425-B505-4DD6-98ED-3F8593358FC8}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {FEDAE425-B505-4DD6-98ED-3F8593358FC8}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {FEDAE425-B505-4DD6-98ED-3F8593358FC8}.Release|Any CPU.Build.0 = Release|Any CPU
{E95A1258-F666-4D4E-9101-E0C46F6A3CB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E95A1258-F666-4D4E-9101-E0C46F6A3CB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E95A1258-F666-4D4E-9101-E0C46F6A3CB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E95A1258-F666-4D4E-9101-E0C46F6A3CB3}.Release|Any CPU.Build.0 = Release|Any CPU
- {FA10201B-AE2A-4BFC-8E8F-6F944680974D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {FA10201B-AE2A-4BFC-8E8F-6F944680974D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {FA10201B-AE2A-4BFC-8E8F-6F944680974D}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {FA10201B-AE2A-4BFC-8E8F-6F944680974D}.Release|Any CPU.Build.0 = Release|Any CPU
- {8F5BBBDA-B3DD-458B-B97F-F67531D323E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {8F5BBBDA-B3DD-458B-B97F-F67531D323E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {8F5BBBDA-B3DD-458B-B97F-F67531D323E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {8F5BBBDA-B3DD-458B-B97F-F67531D323E0}.Release|Any CPU.Build.0 = Release|Any CPU
{95D78D57-3232-491D-BAD6-F373D76EA34D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{95D78D57-3232-491D-BAD6-F373D76EA34D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{95D78D57-3232-491D-BAD6-F373D76EA34D}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -95,22 +80,35 @@ Global
{C212983F-67DB-44EB-BFB0-5DA75A86DF55}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C212983F-67DB-44EB-BFB0-5DA75A86DF55}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C212983F-67DB-44EB-BFB0-5DA75A86DF55}.Release|Any CPU.Build.0 = Release|Any CPU
+ {92107DF5-73DF-4371-8EB1-6734FED704AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {92107DF5-73DF-4371-8EB1-6734FED704AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {92107DF5-73DF-4371-8EB1-6734FED704AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {92107DF5-73DF-4371-8EB1-6734FED704AD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C68C26EB-7659-402A-93D1-E6E248DA5427}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C68C26EB-7659-402A-93D1-E6E248DA5427}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C68C26EB-7659-402A-93D1-E6E248DA5427}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C68C26EB-7659-402A-93D1-E6E248DA5427}.Release|Any CPU.Build.0 = Release|Any CPU
+ {76E622F6-7CDD-4B1F-AD06-FFABF37C55E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {76E622F6-7CDD-4B1F-AD06-FFABF37C55E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {76E622F6-7CDD-4B1F-AD06-FFABF37C55E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {76E622F6-7CDD-4B1F-AD06-FFABF37C55E5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {ED3541C9-D2B2-4D06-A464-38E404A3919A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {ED3541C9-D2B2-4D06-A464-38E404A3919A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {ED3541C9-D2B2-4D06-A464-38E404A3919A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {ED3541C9-D2B2-4D06-A464-38E404A3919A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
- {42BEFACE-39F9-4FE4-B725-15CD2B16292E} = {47C98B55-08F1-4428-863E-2C5C876DEEFE}
- {FEDAE425-B505-4DD6-98ED-3F8593358FC8} = {0B0EDB0F-FF67-4B78-A8DB-B5C23E1FEE8C}
- {6326E0E2-3F48-4BAF-80D3-47AED5EB647C} = {63F75859-4698-4EDE-8B70-4ACBB8BC425A}
{E95A1258-F666-4D4E-9101-E0C46F6A3CB3} = {0B0EDB0F-FF67-4B78-A8DB-B5C23E1FEE8C}
- {C42106CF-F685-4F29-BC18-A70616BD68A0} = {63F75859-4698-4EDE-8B70-4ACBB8BC425A}
- {05CAF9B2-981E-40C0-AE31-5FA56E351F12} = {C42106CF-F685-4F29-BC18-A70616BD68A0}
- {FA10201B-AE2A-4BFC-8E8F-6F944680974D} = {47C98B55-08F1-4428-863E-2C5C876DEEFE}
- {8F5BBBDA-B3DD-458B-B97F-F67531D323E0} = {0B0EDB0F-FF67-4B78-A8DB-B5C23E1FEE8C}
{95D78D57-3232-491D-BAD6-F373D76EA34D} = {D61415CA-D822-43DD-9AE7-993B8B60E855}
{87FC440E-6A4D-47D8-9EB2-416FC31CC4A6} = {47C98B55-08F1-4428-863E-2C5C876DEEFE}
{C212983F-67DB-44EB-BFB0-5DA75A86DF55} = {0B0EDB0F-FF67-4B78-A8DB-B5C23E1FEE8C}
+ {92107DF5-73DF-4371-8EB1-6734FED704AD} = {0B0EDB0F-FF67-4B78-A8DB-B5C23E1FEE8C}
+ {C68C26EB-7659-402A-93D1-E6E248DA5427} = {0B0EDB0F-FF67-4B78-A8DB-B5C23E1FEE8C}
+ {76E622F6-7CDD-4B1F-AD06-FFABF37C55E5} = {47C98B55-08F1-4428-863E-2C5C876DEEFE}
+ {ED3541C9-D2B2-4D06-A464-38E404A3919A} = {47C98B55-08F1-4428-863E-2C5C876DEEFE}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {387AC1AC-F90C-4EF8-955A-04D495C75AF4}
diff --git a/GraphQL.Client.sln.DotSettings b/GraphQL.Client.sln.DotSettings
new file mode 100644
index 00000000..9e5ec22f
--- /dev/null
+++ b/GraphQL.Client.sln.DotSettings
@@ -0,0 +1,2 @@
+
+ QL
\ No newline at end of file
diff --git a/README.md b/README.md
index 21f7fefe..f55b48dd 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# GraphQL.Client
[](https://www.nuget.org/packages/GraphQL.Client)
-[](https://www.myget.org/feed/graphql-dotnet/package/nuget/GraphQL.Client)
+[](https://www.nuget.org/packages/GraphQL.Client)
A GraphQL Client for .NET Standard over HTTP.
@@ -43,25 +43,64 @@ var heroAndFriendsRequest = new GraphQLRequest {
};
```
-### Send Request:
+### Execute Query/Mutation:
```csharp
var graphQLClient = new GraphQLClient("https://swapi.apis.guru/");
-var graphQLResponse = await graphQLClient.PostAsync(heroRequest);
+
+public class HeroAndFriendsResponse {
+ public Hero Hero {get; set;}
+
+ public class Hero {
+ public string Name {get; set;}
+
+ public List Friends {get; set;}
+ }
+}
+
+var graphQLResponse = await graphQLClient.SendQueryAsync(heroAndFriendsRequest);
+
+var heroName = graphQLResponse.Data.Hero.Name;
```
-### Read GraphQLResponse:
+### Use Subscriptions
-#### Dynamic:
```csharp
-var graphQLResponse = await graphQLClient.PostAsync(heroRequest);
-var dynamicHeroName = graphQLResponse.Data.hero.name.Value; //Value of data->hero->name
+public class UserJoinedSubscriptionResult {
+ public ChatUser UserJoined { get; set; }
+
+ public class ChatUser {
+ public string DisplayName { get; set; }
+ public string Id { get; set; }
+ }
+}
```
-#### Typed:
+#### Create subscription
+
+```csharp
+var userJoinedRequest = new GraphQLRequest {
+ Query = @"
+ subscription {
+ userJoined{
+ displayName
+ id
+ }
+ }"
+};
+
+IObservable> subscriptionStream
+ = client.CreateSubscriptionStream(userJoinedRequest);
+
+var subscription = subscriptionStream.Subscribe(response =>
+ {
+ Console.WriteLine($"user '{response.Data.UserJoined.DisplayName}' joined")
+ });
+```
+
+#### End Subscription
+
```csharp
-var graphQLResponse = await graphQLClient.PostAsync(heroRequest);
-var personType = graphQLResponse.GetDataFieldAs("hero"); //data->hero is casted as Person
-var name = personType.Name;
+subscription.Dispose();
```
## Useful Links:
diff --git a/SubscriptionIntegrationTest.ConsoleClient/Program.cs b/SubscriptionIntegrationTest.ConsoleClient/Program.cs
new file mode 100644
index 00000000..95000512
--- /dev/null
+++ b/SubscriptionIntegrationTest.ConsoleClient/Program.cs
@@ -0,0 +1,135 @@
+using System;
+using System.Net.WebSockets;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using System.Threading.Tasks;
+using GraphQL.Client.Http;
+using GraphQL.Common.Request;
+
+namespace SubsccriptionIntegrationTest.ConsoleClient
+{
+ class Program
+ {
+ static async Task Main(string[] args)
+ {
+ Console.WriteLine("configuring client ...");
+ using (var client = new GraphQLHttpClient("http://localhost:5000/graphql/", new GraphQLHttpClientOptions{ UseWebSocketForQueriesAndMutations = true }))
+ {
+
+ Console.WriteLine("subscribing to message stream ...");
+
+ var subscriptions = new CompositeDisposable();
+
+ subscriptions.Add(client.WebSocketReceiveErrors.Subscribe(e => {
+ if(e is WebSocketException we)
+ Console.WriteLine($"WebSocketException: {we.Message} (WebSocketError {we.WebSocketErrorCode}, ErrorCode {we.ErrorCode}, NativeErrorCode {we.NativeErrorCode}");
+ else
+ Console.WriteLine($"Exception in websocket receive stream: {e.ToString()}");
+ }));
+
+ subscriptions.Add(CreateSubscription("1", client));
+ await Task.Delay(200);
+ subscriptions.Add(CreateSubscription2("2", client));
+ await Task.Delay(200);
+ subscriptions.Add(CreateSubscription("3", client));
+ await Task.Delay(200);
+ subscriptions.Add(CreateSubscription("4", client));
+ await Task.Delay(200);
+ subscriptions.Add(CreateSubscription("5", client));
+ await Task.Delay(200);
+ subscriptions.Add(CreateSubscription("6", client));
+ await Task.Delay(200);
+ subscriptions.Add(CreateSubscription("7", client));
+
+ using (subscriptions)
+ {
+ Console.WriteLine("client setup complete");
+ var quit = false;
+ do
+ {
+ Console.WriteLine("write message and press enter...");
+ var message = Console.ReadLine();
+ var graphQLRequest = new GraphQLRequest(@"
+ mutation($input: MessageInputType){
+ addMessage(message: $input){
+ content
+ }
+ }")
+ {
+ Variables = new
+ {
+ input = new
+ {
+ fromId = "2",
+ content = message,
+ sentAt = DateTime.Now
+ }
+ }
+ };
+ var result = await client.SendMutationAsync(graphQLRequest).ConfigureAwait(false);
+
+ if(result.Errors != null && result.Errors.Length > 0)
+ {
+ Console.WriteLine($"request returned {result.Errors.Length} errors:");
+ foreach (var item in result.Errors)
+ {
+ Console.WriteLine($"{item.Message}");
+ }
+ }
+ }
+ while(!quit);
+ Console.WriteLine("shutting down ...");
+ }
+ Console.WriteLine("subscriptions disposed ...");
+ }
+ Console.WriteLine("client disposed ...");
+ }
+
+ private static IDisposable CreateSubscription(string id, GraphQLHttpClient client)
+ {
+#pragma warning disable 618
+ var stream = client.CreateSubscriptionStream(new GraphQLRequest(@"
+ subscription {
+ messageAdded{
+ content
+ from {
+ displayName
+ }
+ }
+ }"
+ )
+ { Variables = new { id } });
+#pragma warning restore 618
+
+ return stream.Subscribe(
+ response => Console.WriteLine($"{id}: new message from \"{response.Data.messageAdded.from.displayName.Value}\": {response.Data.messageAdded.content.Value}"),
+ exception => Console.WriteLine($"{id}: message subscription stream failed: {exception}"),
+ () => Console.WriteLine($"{id}: message subscription stream completed"));
+
+ }
+
+
+ private static IDisposable CreateSubscription2(string id, GraphQLHttpClient client)
+ {
+#pragma warning disable 618
+ var stream = client.CreateSubscriptionStream(new GraphQLRequest(@"
+ subscription {
+ contentAdded{
+ content
+ from {
+ displayName
+ }
+ }
+ }"
+ )
+ { Variables = new { id } });
+#pragma warning restore 618
+
+ return stream.Subscribe(
+ response => Console.WriteLine($"{id}: new content from \"{response.Data.contentAdded.from.displayName.Value}\": {response.Data.contentAdded.content.Value}"),
+ exception => Console.WriteLine($"{id}: content subscription stream failed: {exception}"),
+ () => Console.WriteLine($"{id}: content subscription stream completed"));
+
+ }
+ }
+}
diff --git a/SubscriptionIntegrationTest.ConsoleClient/SubscriptionIntegrationTest.ConsoleClient.csproj b/SubscriptionIntegrationTest.ConsoleClient/SubscriptionIntegrationTest.ConsoleClient.csproj
new file mode 100644
index 00000000..83d4884b
--- /dev/null
+++ b/SubscriptionIntegrationTest.ConsoleClient/SubscriptionIntegrationTest.ConsoleClient.csproj
@@ -0,0 +1,18 @@
+
+
+
+ Exe
+ netcoreapp3.0;net461
+ 8.0
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet-tools.json b/dotnet-tools.json
index 80b96672..6d123e6b 100644
--- a/dotnet-tools.json
+++ b/dotnet-tools.json
@@ -1,11 +1,17 @@
{
- "isRoot": true,
- "tools": {
- "dotnet-format": {
- "commands": [
- "dotnet-format"
- ],
- "version": "3.1.37601"
- }
- }
-}
+ "isRoot": true,
+ "tools": {
+ "dotnet-format": {
+ "version": "3.2.107702",
+ "commands": [
+ "dotnet-format"
+ ]
+ },
+ "gitversion.tool": {
+ "version": "5.1.3",
+ "commands": [
+ "dotnet-gitversion"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/GraphQL.Client.Http.Examples/GraphQL.Client.Http.Examples.csproj b/examples/GraphQL.Client.Http.Examples/GraphQL.Client.Http.Examples.csproj
index 959525f5..2f2dea3c 100644
--- a/examples/GraphQL.Client.Http.Examples/GraphQL.Client.Http.Examples.csproj
+++ b/examples/GraphQL.Client.Http.Examples/GraphQL.Client.Http.Examples.csproj
@@ -3,6 +3,7 @@
Exe
netcoreapp3.1
+ false
@@ -10,7 +11,7 @@
-
+
diff --git a/examples/GraphQL.Client.Http.Examples/Program.cs b/examples/GraphQL.Client.Http.Examples/Program.cs
index 50618a1d..0cce0283 100644
--- a/examples/GraphQL.Client.Http.Examples/Program.cs
+++ b/examples/GraphQL.Client.Http.Examples/Program.cs
@@ -1,6 +1,5 @@
using System;
using System.Text.Json;
-using System.Text.Json.Serialization;
using System.Threading.Tasks;
using GraphQL.Server.Test.GraphQL.Models;
using Microsoft.AspNetCore.TestHost;
@@ -13,11 +12,11 @@ public class Program {
AllowSynchronousIO = true
};
- public async static Task Main(string[] args) {
+ public static async Task Main(string[] args) {
using var httpClient = testServer.CreateClient();
using var graphqlClient = httpClient.AsGraphQLClient($"{testServer.BaseAddress}graphql");
- var graphQLHttpRequest = new GraphQLHttpRequest {
- Query = @"
+ var graphQLRequest = new GraphQLRequest(
+ @"
{
repository(owner: ""graphql-dotnet"", name: ""graphql-client"") {
databaseId,
@@ -26,9 +25,9 @@ public async static Task Main(string[] args) {
url
}
}"
- };
- var graphQLHttpResponse = await graphqlClient.SendHttpQueryAsync(graphQLHttpRequest);
- Console.WriteLine(JsonSerializer.Serialize(graphQLHttpResponse, new JsonSerializerOptions { WriteIndented = true }));
+ );
+ var graphQLResponse = await graphqlClient.SendQueryAsync(graphQLRequest);
+ Console.WriteLine(JsonSerializer.Serialize(graphQLResponse, new JsonSerializerOptions { WriteIndented = true }));
}
private class Schema {
diff --git a/root.props b/root.props
index 8725d564..2c0a5b7d 100644
--- a/root.props
+++ b/root.props
@@ -2,13 +2,13 @@
- Deinok,graphql-dotnet
+ Deinok,Alexander Rose,graphql-dotnet
A GraphQL Client for .NET Standard
True
True
8.0
en-US
- CS0618;CS1591;CS1701;CS8618;NU5048;NU5105;NU5125
+ CS0618;CS1591;CS1701;CS8618;CS8632;NU5048;NU5105;NU5125
annotations
icon.png
LICENSE.txt
@@ -16,9 +16,8 @@
true
GraphQL
git
- https://github.com/graphql-dotnet/graphql-client.git
+ https://github.com/graphql-dotnet/graphql-client
True
- 2.0.0-alpha.4
4
diff --git a/src/GraphQL.Client.Abstractions/GraphQL.Client.Abstractions.csproj b/src/GraphQL.Client.Abstractions/GraphQL.Client.Abstractions.csproj
new file mode 100644
index 00000000..6430f4b7
--- /dev/null
+++ b/src/GraphQL.Client.Abstractions/GraphQL.Client.Abstractions.csproj
@@ -0,0 +1,19 @@
+
+
+
+
+
+ A GraphQL Client
+ GraphQL.Client.Abstractions
+ netstandard2.0
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/GraphQL.Client.Abstractions/GraphQLClientExtensions.cs b/src/GraphQL.Client.Abstractions/GraphQLClientExtensions.cs
new file mode 100644
index 00000000..5bd30c9d
--- /dev/null
+++ b/src/GraphQL.Client.Abstractions/GraphQLClientExtensions.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace GraphQL.Client.Abstractions {
+ public static class GraphQLClientExtensions {
+ public static Task> SendQueryAsync(this IGraphQLClient client,
+ string query, object? variables = null,
+ string? operationName = null, Func defineResponseType = null, CancellationToken cancellationToken = default) {
+ return client.SendQueryAsync(new GraphQLRequest(query, variables, operationName), cancellationToken: cancellationToken);
+ }
+ public static Task> SendMutationAsync(this IGraphQLClient client,
+ string query, object? variables = null,
+ string? operationName = null, Func defineResponseType = null, CancellationToken cancellationToken = default) {
+ return client.SendMutationAsync(new GraphQLRequest(query, variables, operationName), cancellationToken: cancellationToken);
+ }
+
+ public static Task> SendQueryAsync(this IGraphQLClient client,
+ GraphQLRequest request, Func defineResponseType, CancellationToken cancellationToken = default)
+ => client.SendQueryAsync(request, cancellationToken);
+
+ public static Task> SendMutationAsync(this IGraphQLClient client,
+ GraphQLRequest request, Func defineResponseType, CancellationToken cancellationToken = default)
+ => client.SendMutationAsync(request, cancellationToken);
+
+ public static IObservable> CreateSubscriptionStream(
+ this IGraphQLClient client, GraphQLRequest request, Func defineResponseType)
+ => client.CreateSubscriptionStream(request);
+
+ public static IObservable> CreateSubscriptionStream(
+ this IGraphQLClient client, GraphQLRequest request, Func defineResponseType, Action exceptionHandler)
+ => client.CreateSubscriptionStream(request, exceptionHandler);
+ }
+}
diff --git a/src/GraphQL.Client.Abstractions/IGraphQLClient.cs b/src/GraphQL.Client.Abstractions/IGraphQLClient.cs
new file mode 100644
index 00000000..3b47a932
--- /dev/null
+++ b/src/GraphQL.Client.Abstractions/IGraphQLClient.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Net.WebSockets;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace GraphQL.Client.Abstractions {
+
+ public interface IGraphQLClient : IDisposable {
+
+ Task> SendQueryAsync(GraphQLRequest request, CancellationToken cancellationToken = default);
+
+ Task> SendMutationAsync(GraphQLRequest request, CancellationToken cancellationToken = default);
+
+ ///
+ /// Creates a subscription to a GraphQL server. The connection is not established until the first actual subscription is made.
+ /// All subscriptions made to this stream share the same hot observable.
+ /// The stream must be recreated completely after an error has occured within its logic (i.e. a )
+ ///
+ /// the GraphQL request for this subscription
+ /// an observable stream for the specified subscription
+ IObservable> CreateSubscriptionStream(GraphQLRequest request);
+
+ ///
+ /// Creates a subscription to a GraphQL server. The connection is not established until the first actual subscription is made.
+ /// All subscriptions made to this stream share the same hot observable.
+ /// All s are passed to the to be handled externally.
+ /// If the completes normally, the subscription is recreated with a new connection attempt.
+ /// Any exception thrown by will cause the sequence to fail.
+ ///
+ /// the GraphQL request for this subscription
+ /// an external handler for all s occuring within the sequence
+ /// an observable stream for the specified subscription
+ IObservable> CreateSubscriptionStream(GraphQLRequest request, Action exceptionHandler);
+
+ ///
+ /// Publishes all exceptions which occur inside the websocket receive stream (i.e. for logging purposes)
+ ///
+ IObservable WebSocketReceiveErrors { get; }
+ }
+
+}
diff --git a/src/GraphQL.Client.Http/GraphQL.Client.Http.csproj b/src/GraphQL.Client.Http/GraphQL.Client.Http.csproj
deleted file mode 100644
index 05900a76..00000000
--- a/src/GraphQL.Client.Http/GraphQL.Client.Http.csproj
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
- netstandard2.0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/GraphQL.Client.Http/GraphQLHttpClient.cs b/src/GraphQL.Client.Http/GraphQLHttpClient.cs
deleted file mode 100644
index 634bcd2b..00000000
--- a/src/GraphQL.Client.Http/GraphQLHttpClient.cs
+++ /dev/null
@@ -1,108 +0,0 @@
-using System;
-using System.Net.Http;
-using System.Text;
-using System.Text.Json;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace GraphQL.Client.Http {
-
- public class GraphQLHttpClient : IDisposable, IGraphQLClient {
-
- public Uri EndPoint { get; set; }
-
- public JsonSerializerOptions JsonSerializerOptions { get; set; } = new JsonSerializerOptions {
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase
- };
-
- private readonly HttpClient httpClient;
-
- public GraphQLHttpClient(string endPoint) {
- this.EndPoint = new Uri(endPoint);
- this.httpClient = new HttpClient();
- }
-
- public GraphQLHttpClient(Uri endPoint) {
- this.EndPoint = endPoint;
- this.httpClient = new HttpClient();
- }
-
- public GraphQLHttpClient(string endPoint, GraphQLHttpClientOptions options) {
- this.EndPoint = new Uri(endPoint);
- this.httpClient = new HttpClient();
- }
-
- public GraphQLHttpClient(Uri endPoint, GraphQLHttpClientOptions options) {
- this.EndPoint = endPoint;
- this.httpClient = new HttpClient();
- }
-
- public GraphQLHttpClient(string endPoint, HttpClient httpClient) {
- this.EndPoint = new Uri(endPoint);
- this.httpClient = httpClient;
- }
-
- public GraphQLHttpClient(Uri endPoint, HttpClient httpClient) {
- this.EndPoint = endPoint;
- this.httpClient = httpClient;
- }
-
- public GraphQLHttpClient(string endPoint, GraphQLHttpClientOptions options, HttpClient httpClient) {
- this.EndPoint = new Uri(endPoint);
- this.httpClient = httpClient;
- }
-
- public GraphQLHttpClient(Uri endPoint, GraphQLHttpClientOptions options, HttpClient httpClient) {
- this.EndPoint = endPoint;
- this.httpClient = httpClient;
- }
-
- public void Dispose() => this.httpClient.Dispose();
-
- public async Task> SendHttpQueryAsync(GraphQLHttpRequest request, CancellationToken cancellationToken = default) {
- using var httpRequestMessage = this.GenerateHttpRequestMessage(request);
- using var httpResponseMessage = await this.httpClient.SendAsync(httpRequestMessage, cancellationToken);
- if (!httpResponseMessage.IsSuccessStatusCode) {
- throw new GraphQLHttpException(httpResponseMessage);
- }
-
- var bodyStream = await httpResponseMessage.Content.ReadAsStreamAsync();
- return await JsonSerializer.DeserializeAsync>(bodyStream, this.JsonSerializerOptions, cancellationToken);
- }
-
- public async Task> SendHttpQueryAsync(GraphQLHttpRequest request, CancellationToken cancellationToken = default) =>
- await this.SendHttpQueryAsync(request, cancellationToken);
-
- public async Task> SendHttpMutationAsync(GraphQLHttpRequest request, CancellationToken cancellationToken = default) {
- await Task.CompletedTask;
- throw new NotImplementedException();
- }
-
- public async Task> SendQueryAsync(GraphQLRequest request, CancellationToken cancellationToken = default) {
- await Task.CompletedTask;
- throw new NotImplementedException();
- }
-
- public async Task> SendMutationAsync(GraphQLRequest request, CancellationToken cancellationToken = default) {
- await Task.CompletedTask;
- throw new NotImplementedException();
- }
-
- private HttpRequestMessage GenerateHttpRequestMessage(GraphQLRequest request) {
- return new HttpRequestMessage(HttpMethod.Post, this.EndPoint) {
- Content = new StringContent(JsonSerializer.Serialize(request, this.JsonSerializerOptions), Encoding.UTF8, "application/json")
- };
- }
-
- public async Task> SendQueryAsync(GraphQLRequest request, CancellationToken cancellationToken = default) {
- await Task.CompletedTask;
- throw new NotImplementedException();
- }
-
- public async Task> SendMutationAsync(GraphQLRequest request, CancellationToken cancellationToken = default) {
- await Task.CompletedTask;
- throw new NotImplementedException();
- }
- }
-
-}
diff --git a/src/GraphQL.Client.Http/GraphQLHttpClientOptions.cs b/src/GraphQL.Client.Http/GraphQLHttpClientOptions.cs
deleted file mode 100644
index 73a00430..00000000
--- a/src/GraphQL.Client.Http/GraphQLHttpClientOptions.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Net.Http;
-using System.Net.Http.Headers;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Converters;
-using Newtonsoft.Json.Serialization;
-
-namespace GraphQL.Client.Http {
-
- ///
- /// The Options that the will use
- ///
- public class GraphQLHttpClientOptions {
-
- ///
- /// The GraphQL EndPoint to be used
- ///
- public Uri EndPoint { get; set; }
-
- ///
- /// The that is going to be used
- ///
- public JsonSerializerSettings JsonSerializerSettings { get; set; } = new JsonSerializerSettings {
- ContractResolver = new CamelCasePropertyNamesContractResolver(),
- Converters = new List
- {
- new StringEnumConverter()
- }
- };
-
- ///
- /// The that is going to be used
- ///
- public HttpMessageHandler HttpMessageHandler { get; set; } = new HttpClientHandler();
-
- ///
- /// The that will be send on POST
- ///
- public MediaTypeHeaderValue MediaType { get; set; } = MediaTypeHeaderValue.Parse("application/json; charset=utf-8"); // This should be "application/graphql" also "application/x-www-form-urlencoded" is Accepted
-
- }
-
-}
diff --git a/src/GraphQL.Client.Http/GraphQLHttpRequest.cs b/src/GraphQL.Client.Http/GraphQLHttpRequest.cs
deleted file mode 100644
index 86c0b211..00000000
--- a/src/GraphQL.Client.Http/GraphQLHttpRequest.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace GraphQL.Client.Http {
-
- public class GraphQLHttpRequest : GraphQLRequest {
- }
-
- public class GraphQLHttpRequest : GraphQLHttpRequest {
- }
-
-}
diff --git a/src/GraphQL.Client.Http/HttpClientExtensions.cs b/src/GraphQL.Client.Http/HttpClientExtensions.cs
deleted file mode 100644
index 4f511101..00000000
--- a/src/GraphQL.Client.Http/HttpClientExtensions.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using System;
-using System.Net.Http;
-
-namespace GraphQL.Client.Http {
-
- public static class HttpClientExtensions {
-
- public static GraphQLHttpClient AsGraphQLClient(this HttpClient httpClient, string endPoint) =>
- new GraphQLHttpClient(endPoint, httpClient);
-
- public static GraphQLHttpClient AsGraphQLClient(this HttpClient httpClient, Uri endPoint) =>
- new GraphQLHttpClient(endPoint, httpClient);
-
- public static GraphQLHttpClient AsGraphQLClient(this HttpClient httpClient, string endPoint, GraphQLHttpClientOptions graphQLHttpClientOptions) =>
- new GraphQLHttpClient(endPoint, graphQLHttpClientOptions, httpClient);
-
- public static GraphQLHttpClient AsGraphQLClient(this HttpClient httpClient, Uri endPoint, GraphQLHttpClientOptions graphQLHttpClientOptions) =>
- new GraphQLHttpClient(endPoint, graphQLHttpClientOptions, httpClient);
-
- }
-
-}
diff --git a/src/GraphQL.Client/GraphQL.Client.csproj b/src/GraphQL.Client/GraphQL.Client.csproj
index 110fc770..2dfd5ad8 100644
--- a/src/GraphQL.Client/GraphQL.Client.csproj
+++ b/src/GraphQL.Client/GraphQL.Client.csproj
@@ -1,18 +1,31 @@
-
+
-
- A GraphQL Client
- netstandard2.0
+
+ netstandard2.0;net461
+ GraphQL.Client.Http
+
+
+
+ NETSTANDARD
+
+
+
+ NETFRAMEWORK
+
-
+
+
+
+
+
-
+
diff --git a/src/GraphQL.Client/GraphQLException.cs b/src/GraphQL.Client/GraphQLException.cs
deleted file mode 100644
index 613ab2ac..00000000
--- a/src/GraphQL.Client/GraphQLException.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using System;
-
-namespace GraphQL.Client {
-
- public class GraphQLException : Exception {
-
- public GraphQLError[] Errors { get; }
-
- public GraphQLException(GraphQLError[] errors) : base(errors[0].Message) {
- this.Errors = errors;
- }
-
- }
-
-}
diff --git a/src/GraphQL.Client/GraphQLHttpClient.cs b/src/GraphQL.Client/GraphQLHttpClient.cs
new file mode 100644
index 00000000..15ff6e8f
--- /dev/null
+++ b/src/GraphQL.Client/GraphQLHttpClient.cs
@@ -0,0 +1,164 @@
+using System;
+using System.Collections.Concurrent;
+using System.Net.Http;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using GraphQL.Client.Abstractions;
+using GraphQL.Client.Http.Websocket;
+
+namespace GraphQL.Client.Http {
+
+ public class GraphQLHttpClient : IGraphQLClient {
+
+ private readonly GraphQLHttpWebSocket graphQlHttpWebSocket;
+ private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
+ private readonly ConcurrentDictionary, object> subscriptionStreams = new ConcurrentDictionary, object>();
+
+ ///
+ /// the instance of which is used internally
+ ///
+ public HttpClient HttpClient { get; }
+
+ ///
+ /// The Options to be used
+ ///
+ public GraphQLHttpClientOptions Options { get; }
+
+ ///
+ public IObservable WebSocketReceiveErrors => graphQlHttpWebSocket.ReceiveErrors;
+
+ public GraphQLHttpClient(string endPoint) : this(new Uri(endPoint)) { }
+
+ public GraphQLHttpClient(Uri endPoint) : this(o => o.EndPoint = endPoint) { }
+
+ public GraphQLHttpClient(Action configure) {
+ Options = new GraphQLHttpClientOptions();
+ configure(Options);
+ this.HttpClient = new HttpClient(Options.HttpMessageHandler);
+ this.graphQlHttpWebSocket = new GraphQLHttpWebSocket(GetWebSocketUri(), Options);
+ }
+
+ public GraphQLHttpClient(GraphQLHttpClientOptions options) {
+ Options = options;
+ this.HttpClient = new HttpClient(Options.HttpMessageHandler);
+ this.graphQlHttpWebSocket = new GraphQLHttpWebSocket(GetWebSocketUri(), Options);
+ }
+
+ public GraphQLHttpClient(GraphQLHttpClientOptions options, HttpClient httpClient) {
+ Options = options;
+ this.HttpClient = httpClient;
+ this.graphQlHttpWebSocket = new GraphQLHttpWebSocket(GetWebSocketUri(), Options);
+ }
+
+ ///
+ public Task> SendQueryAsync(GraphQLRequest request, CancellationToken cancellationToken = default) {
+ return Options.UseWebSocketForQueriesAndMutations
+ ? this.graphQlHttpWebSocket.SendRequest(request, this, cancellationToken)
+ : this.SendHttpPostRequestAsync(request, cancellationToken);
+ }
+
+ ///
+ public Task> SendMutationAsync(GraphQLRequest request,
+ CancellationToken cancellationToken = default)
+ => SendQueryAsync(request, cancellationToken);
+
+ ///
+ public IObservable> CreateSubscriptionStream(GraphQLRequest request) {
+ if (disposed)
+ throw new ObjectDisposedException(nameof(GraphQLHttpClient));
+
+ var key = new Tuple(request, typeof(TResponse));
+
+ if (subscriptionStreams.ContainsKey(key))
+ return (IObservable>)subscriptionStreams[key];
+
+ var observable = graphQlHttpWebSocket.CreateSubscriptionStream(request, this, cancellationToken: cancellationTokenSource.Token);
+
+ subscriptionStreams.TryAdd(key, observable);
+ return observable;
+ }
+
+ ///
+ public IObservable> CreateSubscriptionStream(GraphQLRequest request, Action exceptionHandler) {
+ if (disposed)
+ throw new ObjectDisposedException(nameof(GraphQLHttpClient));
+
+ var key = new Tuple(request, typeof(TResponse));
+
+ if (subscriptionStreams.ContainsKey(key))
+ return (IObservable>)subscriptionStreams[key];
+
+ var observable = graphQlHttpWebSocket.CreateSubscriptionStream(request, this, exceptionHandler, cancellationTokenSource.Token);
+ subscriptionStreams.TryAdd(key, observable);
+ return observable;
+ }
+
+ ///
+ /// explicitly opens the websocket connection. Will be closed again on disposing the last subscription
+ ///
+ ///
+ public Task InitializeWebsocketConnection() => graphQlHttpWebSocket.InitializeWebSocket();
+
+ #region Private Methods
+
+ private async Task> SendHttpPostRequestAsync(GraphQLRequest request, CancellationToken cancellationToken = default) {
+ var preprocessedRequest = await Options.PreprocessRequest(request, this);
+ using var httpRequestMessage = this.GenerateHttpRequestMessage(preprocessedRequest);
+ using var httpResponseMessage = await this.HttpClient.SendAsync(httpRequestMessage, cancellationToken);
+ if (!httpResponseMessage.IsSuccessStatusCode) {
+ throw new GraphQLHttpException(httpResponseMessage);
+ }
+
+ var bodyStream = await httpResponseMessage.Content.ReadAsStreamAsync();
+ return await bodyStream.DeserializeFromJsonAsync>(Options, cancellationToken);
+ }
+
+ private HttpRequestMessage GenerateHttpRequestMessage(GraphQLRequest request) {
+ var message = new HttpRequestMessage(HttpMethod.Post, this.Options.EndPoint) {
+ Content = new StringContent(request.SerializeToJson(Options), Encoding.UTF8, Options.MediaType)
+ };
+
+ if (request is GraphQLHttpRequest httpRequest)
+ httpRequest.PreprocessHttpRequestMessage(message);
+
+ return message;
+ }
+
+ private Uri GetWebSocketUri() {
+ var webSocketSchema = this.Options.EndPoint.Scheme == "https" ? "wss" : "ws";
+ return new Uri($"{webSocketSchema}://{this.Options.EndPoint.Host}:{this.Options.EndPoint.Port}{this.Options.EndPoint.AbsolutePath}");
+ }
+
+ #endregion
+
+
+ #region IDisposable
+
+ ///
+ /// Releases unmanaged resources
+ ///
+ public void Dispose() {
+ lock (disposeLocker) {
+ if (!disposed) {
+ _dispose();
+ }
+ }
+ }
+
+ private bool disposed = false;
+ private readonly object disposeLocker = new object();
+
+ private void _dispose() {
+ disposed = true;
+ this.HttpClient.Dispose();
+ this.graphQlHttpWebSocket.Dispose();
+ cancellationTokenSource.Cancel();
+ cancellationTokenSource.Dispose();
+ }
+
+ #endregion
+
+ }
+
+}
diff --git a/src/GraphQL.Client/GraphQLHttpClientExtensions.cs b/src/GraphQL.Client/GraphQLHttpClientExtensions.cs
new file mode 100644
index 00000000..405d041c
--- /dev/null
+++ b/src/GraphQL.Client/GraphQLHttpClientExtensions.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Net.WebSockets;
+using GraphQL.Client.Abstractions;
+
+namespace GraphQL.Client.Http {
+ public static class GraphQLHttpClientExtensions {
+ ///
+ /// Creates a subscription to a GraphQL server. The connection is not established until the first actual subscription is made.
+ /// All subscriptions made to this stream share the same hot observable.
+ /// All s are passed to the to be handled externally.
+ /// If the completes normally, the subscription is recreated with a new connection attempt.
+ /// Other s or any exception thrown by will cause the sequence to fail.
+ ///
+ /// the GraphQL client
+ /// the GraphQL request for this subscription
+ /// an external handler for all s occuring within the sequence
+ /// an observable stream for the specified subscription
+ public static IObservable> CreateSubscriptionStream(this IGraphQLClient client,
+ GraphQLRequest request, Action webSocketExceptionHandler) {
+ return client.CreateSubscriptionStream(request, e => {
+ if (e is WebSocketException webSocketException)
+ webSocketExceptionHandler(webSocketException);
+ else
+ throw e;
+ });
+ }
+
+ public static IObservable> CreateSubscriptionStream(
+ this IGraphQLClient client, GraphQLRequest request, Func defineResponseType, Action webSocketExceptionHandler)
+ => client.CreateSubscriptionStream(request, webSocketExceptionHandler);
+ }
+}
diff --git a/src/GraphQL.Client/GraphQLHttpClientOptions.cs b/src/GraphQL.Client/GraphQLHttpClientOptions.cs
new file mode 100644
index 00000000..dcb0a37e
--- /dev/null
+++ b/src/GraphQL.Client/GraphQLHttpClientOptions.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Serialization;
+
+namespace GraphQL.Client.Http {
+
+ ///
+ /// The Options that the will use
+ ///
+ public class GraphQLHttpClientOptions {
+
+ ///
+ /// The GraphQL EndPoint to be used
+ ///
+ public Uri EndPoint { get; set; }
+
+ ///
+ /// The that is going to be used
+ ///
+ public JsonSerializerSettings JsonSerializerSettings { get; set; } = new JsonSerializerSettings {
+ ContractResolver = new CamelCasePropertyNamesContractResolver()
+ };
+
+ ///
+ /// The that is going to be used
+ ///
+ public HttpMessageHandler HttpMessageHandler { get; set; } = new HttpClientHandler();
+
+ ///
+ /// The that will be send on POST
+ ///
+ public string MediaType { get; set; } = "application/json"; // This should be "application/graphql" also "application/x-www-form-urlencoded" is Accepted
+
+ ///
+ /// The back-off strategy for automatic websocket/subscription reconnects. Calculates the delay before the next connection attempt is made.
+ /// default formula: min(n, 5) * 1,5 * random(0.0, 1.0)
+ ///
+ public Func BackOffStrategy { get; set; } = n => {
+ var rnd = new Random();
+ return TimeSpan.FromSeconds(Math.Min(n, 5) * 1.5 + rnd.NextDouble());
+ };
+
+ ///
+ /// If , the websocket connection is also used for regular queries and mutations
+ ///
+ public bool UseWebSocketForQueriesAndMutations { get; set; } = false;
+
+ ///
+ /// Request preprocessing function. Can be used i.e. to inject authorization info into a GraphQL request payload.
+ ///
+ public Func> PreprocessRequest { get; set; } = (request, client) => Task.FromResult(request);
+ }
+}
diff --git a/src/GraphQL.Client.Http/GraphQLHttpException.cs b/src/GraphQL.Client/GraphQLHttpException.cs
similarity index 100%
rename from src/GraphQL.Client.Http/GraphQLHttpException.cs
rename to src/GraphQL.Client/GraphQLHttpException.cs
diff --git a/src/GraphQL.Client/GraphQLHttpRequest.cs b/src/GraphQL.Client/GraphQLHttpRequest.cs
new file mode 100644
index 00000000..67e06259
--- /dev/null
+++ b/src/GraphQL.Client/GraphQLHttpRequest.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Net.Http;
+using System.Runtime.Serialization;
+
+namespace GraphQL.Client.Http {
+
+ public class GraphQLHttpRequest : GraphQLRequest {
+ public GraphQLHttpRequest()
+ {
+ }
+
+ public GraphQLHttpRequest(string query, object? variables = null, string? operationName = null) : base(query, variables, operationName)
+ {
+ }
+
+ ///
+ /// Allows to preprocess a before it is sent, i.e. add custom headers
+ ///
+ [IgnoreDataMember]
+ public Action PreprocessHttpRequestMessage { get; set; } = message => { };
+ }
+}
diff --git a/src/GraphQL.Client.Http/GraphQLHttpResponse.cs b/src/GraphQL.Client/GraphQLHttpResponse.cs
similarity index 100%
rename from src/GraphQL.Client.Http/GraphQLHttpResponse.cs
rename to src/GraphQL.Client/GraphQLHttpResponse.cs
diff --git a/src/GraphQL.Client/GraphQLSerializationExtensions.cs b/src/GraphQL.Client/GraphQLSerializationExtensions.cs
new file mode 100644
index 00000000..649d2c73
--- /dev/null
+++ b/src/GraphQL.Client/GraphQLSerializationExtensions.cs
@@ -0,0 +1,48 @@
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using GraphQL.Client.Http.Websocket;
+using Newtonsoft.Json;
+
+namespace GraphQL.Client.Http {
+ public static class GraphQLSerializationExtensions {
+
+ public static string SerializeToJson(this GraphQLRequest request,
+ GraphQLHttpClientOptions options) {
+ return JsonConvert.SerializeObject(request, options.JsonSerializerSettings);
+ }
+
+ public static byte[] SerializeToBytes(this GraphQLRequest request,
+ GraphQLHttpClientOptions options) {
+ var json = JsonConvert.SerializeObject(request, options.JsonSerializerSettings);
+ return Encoding.UTF8.GetBytes(json);
+ }
+ public static byte[] SerializeToBytes(this GraphQLWebSocketRequest request,
+ GraphQLHttpClientOptions options) {
+ var json = JsonConvert.SerializeObject(request, options.JsonSerializerSettings);
+ return Encoding.UTF8.GetBytes(json);
+ }
+
+ public static TGraphQLResponse DeserializeFromJson(this string jsonString,
+ GraphQLHttpClientOptions options) {
+ return JsonConvert.DeserializeObject(jsonString, options.JsonSerializerSettings);
+ }
+
+ public static TObject DeserializeFromBytes(this byte[] utf8bytes,
+ GraphQLHttpClientOptions options) {
+ return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(utf8bytes), options.JsonSerializerSettings);
+ }
+
+
+ public static Task DeserializeFromJsonAsync(this Stream stream,
+ GraphQLHttpClientOptions options, CancellationToken cancellationToken = default) {
+ using (StreamReader sr = new StreamReader(stream))
+ using (JsonReader reader = new JsonTextReader(sr)) {
+ JsonSerializer serializer = JsonSerializer.Create(options.JsonSerializerSettings);
+
+ return Task.FromResult(serializer.Deserialize(reader));
+ }
+ }
+ }
+}
diff --git a/src/GraphQL.Client/GraphQLSubscriptionException.cs b/src/GraphQL.Client/GraphQLSubscriptionException.cs
new file mode 100644
index 00000000..8607c5be
--- /dev/null
+++ b/src/GraphQL.Client/GraphQLSubscriptionException.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace GraphQL.Client.Http {
+ [Serializable]
+ public class GraphQLSubscriptionException : Exception {
+ //
+ // For guidelines regarding the creation of new exception types, see
+ // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconerrorraisinghandlingguidelines.asp
+ // and
+ // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncscol/html/csharp07192001.asp
+ //
+
+ public GraphQLSubscriptionException() {
+ }
+
+ public GraphQLSubscriptionException(object error) : base(error.ToString()) {
+ }
+
+ protected GraphQLSubscriptionException(
+ SerializationInfo info,
+ StreamingContext context) : base(info, context) {
+ }
+ }
+}
diff --git a/src/GraphQL.Client/HttpClientExtensions.cs b/src/GraphQL.Client/HttpClientExtensions.cs
new file mode 100644
index 00000000..7e78cae3
--- /dev/null
+++ b/src/GraphQL.Client/HttpClientExtensions.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Net.Http;
+
+namespace GraphQL.Client.Http {
+
+ public static class HttpClientExtensions {
+
+ public static GraphQLHttpClient AsGraphQLClient(this HttpClient httpClient, string endPoint) =>
+ httpClient.AsGraphQLClient(new Uri(endPoint));
+
+ public static GraphQLHttpClient AsGraphQLClient(this HttpClient httpClient, Uri endPoint) =>
+ new GraphQLHttpClient(new GraphQLHttpClientOptions { EndPoint = endPoint }, httpClient);
+
+ public static GraphQLHttpClient AsGraphQLClient(this HttpClient httpClient, GraphQLHttpClientOptions graphQLHttpClientOptions) =>
+ new GraphQLHttpClient(graphQLHttpClientOptions, httpClient);
+ }
+
+}
diff --git a/src/GraphQL.Client/IGraphQLClient.cs b/src/GraphQL.Client/IGraphQLClient.cs
deleted file mode 100644
index d1576e3c..00000000
--- a/src/GraphQL.Client/IGraphQLClient.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace GraphQL.Client {
-
- public interface IGraphQLClient : IDisposable {
-
- Task> SendQueryAsync(GraphQLRequest request, CancellationToken cancellationToken = default);
-
- Task> SendMutationAsync(GraphQLRequest request, CancellationToken cancellationToken = default);
-
- }
-
-}
diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs
new file mode 100644
index 00000000..5a4e76c2
--- /dev/null
+++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebSocket.cs
@@ -0,0 +1,286 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Net.Http;
+using System.Net.WebSockets;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using System.Reactive.Threading.Tasks;
+using System.Threading;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+
+namespace GraphQL.Client.Http.Websocket {
+ internal class GraphQLHttpWebSocket : IDisposable {
+ private readonly Uri webSocketUri;
+ private readonly GraphQLHttpClientOptions _options;
+ private readonly ArraySegment buffer;
+ private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
+
+ private Subject _responseSubject;
+ private readonly Subject _requestSubject = new Subject();
+ private readonly Subject _exceptionSubject = new Subject();
+ private IDisposable _requestSubscription;
+
+ public WebSocketState WebSocketState => clientWebSocket?.State ?? WebSocketState.None;
+
+#if NETFRAMEWORK
+ private WebSocket clientWebSocket = null;
+#else
+ private ClientWebSocket clientWebSocket = null;
+#endif
+ private int _connectionAttempt = 0;
+
+ public GraphQLHttpWebSocket(Uri webSocketUri, GraphQLHttpClientOptions options) {
+ this.webSocketUri = webSocketUri;
+ _options = options;
+ buffer = new ArraySegment(new byte[8192]);
+ _responseStream = _createResponseStream();
+
+ _requestSubscription = _requestSubject.Select(request => Observable.FromAsync(() => _sendWebSocketRequest(request))).Concat().Subscribe();
+ }
+
+ public IObservable ReceiveErrors => _exceptionSubject.AsObservable();
+
+ public IObservable ResponseStream => _responseStream;
+ public readonly IObservable _responseStream;
+
+ public Task SendWebSocketRequest(GraphQLWebSocketRequest request) {
+ _requestSubject.OnNext(request);
+ return request.SendTask();
+ }
+
+ private async Task _sendWebSocketRequest(GraphQLWebSocketRequest request) {
+ try {
+ if (_cancellationTokenSource.Token.IsCancellationRequested) {
+ request.SendCanceled();
+ return;
+ }
+
+ await InitializeWebSocket().ConfigureAwait(false);
+ var requestBytes = request.SerializeToBytes(_options);
+ await this.clientWebSocket.SendAsync(
+ new ArraySegment(requestBytes),
+ WebSocketMessageType.Text,
+ true,
+ _cancellationTokenSource.Token).ConfigureAwait(false);
+ request.SendCompleted();
+ }
+ catch (Exception e) {
+ request.SendFailed(e);
+ }
+ }
+
+ public Task InitializeWebSocketTask { get; private set; } = Task.CompletedTask;
+
+ private readonly object _initializeLock = new object();
+
+#region Private Methods
+
+ private Task _backOff() {
+ _connectionAttempt++;
+
+ if (_connectionAttempt == 1) return Task.CompletedTask;
+
+ var delay = _options.BackOffStrategy(_connectionAttempt - 1);
+ Debug.WriteLine($"connection attempt #{_connectionAttempt}, backing off for {delay.TotalSeconds} s");
+ return Task.Delay(delay);
+ }
+
+ public Task InitializeWebSocket() {
+ // do not attempt to initialize if cancellation is requested
+ if (_disposed != null)
+ throw new OperationCanceledException();
+
+ lock (_initializeLock) {
+ // if an initialization task is already running, return that
+ if (InitializeWebSocketTask != null &&
+ !InitializeWebSocketTask.IsFaulted &&
+ !InitializeWebSocketTask.IsCompleted)
+ return InitializeWebSocketTask;
+
+ // if the websocket is open, return a completed task
+ if (clientWebSocket != null && clientWebSocket.State == WebSocketState.Open)
+ return Task.CompletedTask;
+
+ // else (re-)create websocket and connect
+ //_responseStreamConnection?.Dispose();
+ clientWebSocket?.Dispose();
+
+#if NETFRAMEWORK
+ // fix websocket not supported on win 7 using
+ // https://github.com/PingmanTools/System.Net.WebSockets.Client.Managed
+ clientWebSocket = SystemClientWebSocket.CreateClientWebSocket();
+ switch (clientWebSocket) {
+ case ClientWebSocket nativeWebSocket:
+ nativeWebSocket.Options.AddSubProtocol("graphql-ws");
+ nativeWebSocket.Options.ClientCertificates = ((HttpClientHandler)_options.HttpMessageHandler).ClientCertificates;
+ nativeWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)_options.HttpMessageHandler).UseDefaultCredentials;
+ break;
+ case System.Net.WebSockets.Managed.ClientWebSocket managedWebSocket:
+ managedWebSocket.Options.AddSubProtocol("graphql-ws");
+ managedWebSocket.Options.ClientCertificates = ((HttpClientHandler)_options.HttpMessageHandler).ClientCertificates;
+ managedWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)_options.HttpMessageHandler).UseDefaultCredentials;
+ break;
+ default:
+ throw new NotSupportedException($"unknown websocket type {clientWebSocket.GetType().Name}");
+ }
+#else
+ clientWebSocket = new ClientWebSocket();
+ clientWebSocket.Options.AddSubProtocol("graphql-ws");
+ clientWebSocket.Options.ClientCertificates = ((HttpClientHandler)_options.HttpMessageHandler).ClientCertificates;
+ clientWebSocket.Options.UseDefaultCredentials = ((HttpClientHandler)_options.HttpMessageHandler).UseDefaultCredentials;
+#endif
+ return InitializeWebSocketTask = _connectAsync(_cancellationTokenSource.Token);
+ }
+ }
+
+ private IObservable _createResponseStream() {
+ return Observable.Create(_createResultStream)
+ // complete sequence on OperationCanceledException, this is triggered by the cancellation token on disposal
+ .Catch(exception =>
+ Observable.Empty());
+ }
+
+ private async Task _createResultStream(IObserver observer, CancellationToken token) {
+ if (_responseSubject == null || _responseSubject.IsDisposed) {
+ _responseSubject = new Subject();
+ var observable = await _getReceiveResultStream().ConfigureAwait(false);
+ observable.Subscribe(_responseSubject);
+
+ _responseSubject.Subscribe(_ => { }, ex => {
+ _exceptionSubject.OnNext(ex);
+ _responseSubject?.Dispose();
+ _responseSubject = null;
+ },
+ () => {
+ _responseSubject?.Dispose();
+ _responseSubject = null;
+ });
+ }
+
+ return new CompositeDisposable
+ (
+ _responseSubject.Subscribe(observer),
+ Disposable.Create(() => {
+ Debug.WriteLine("response stream disposed");
+ })
+ );
+ }
+
+ private async Task> _getReceiveResultStream() {
+ await InitializeWebSocket().ConfigureAwait(false);
+ return Observable.Defer(() => _getReceiveTask().ToObservable()).Repeat();
+ }
+
+ private async Task _connectAsync(CancellationToken token) {
+ try {
+ await _backOff().ConfigureAwait(false);
+ Debug.WriteLine($"opening websocket {clientWebSocket.GetHashCode()}");
+ await clientWebSocket.ConnectAsync(webSocketUri, token).ConfigureAwait(false);
+ Debug.WriteLine($"connection established on websocket {clientWebSocket.GetHashCode()}");
+ _connectionAttempt = 1;
+ }
+ catch (Exception e) {
+ _exceptionSubject.OnNext(e);
+ throw;
+ }
+ }
+
+
+ private Task _receiveAsyncTask = null;
+ private readonly object _receiveTaskLocker = new object();
+ ///
+ /// wrapper method to pick up the existing request task if already running
+ ///
+ ///
+ private Task _getReceiveTask() {
+ lock (_receiveTaskLocker) {
+ if (_receiveAsyncTask == null ||
+ _receiveAsyncTask.IsFaulted ||
+ _receiveAsyncTask.IsCompleted)
+ _receiveAsyncTask = _receiveResultAsync();
+ }
+
+ return _receiveAsyncTask;
+ }
+
+ private async Task _receiveResultAsync() {
+ try {
+ Debug.WriteLine($"receiving data on websocket {clientWebSocket.GetHashCode()} ...");
+
+ using (var ms = new MemoryStream()) {
+ WebSocketReceiveResult webSocketReceiveResult = null;
+ do {
+ _cancellationTokenSource.Token.ThrowIfCancellationRequested();
+ webSocketReceiveResult = await clientWebSocket.ReceiveAsync(buffer, CancellationToken.None);
+ ms.Write(buffer.Array, buffer.Offset, webSocketReceiveResult.Count);
+ }
+ while (!webSocketReceiveResult.EndOfMessage);
+
+ _cancellationTokenSource.Token.ThrowIfCancellationRequested();
+ ms.Seek(0, SeekOrigin.Begin);
+
+ if (webSocketReceiveResult.MessageType == WebSocketMessageType.Text) {
+ using (StreamReader sr = new StreamReader(ms))
+ using (JsonReader reader = new JsonTextReader(sr)) {
+ JsonSerializer serializer = JsonSerializer.Create(_options.JsonSerializerSettings);
+
+ var response = serializer.Deserialize(reader);
+ response.MessageBytes = ms.ToArray();
+ return response;
+ }
+ }
+ else {
+ throw new NotSupportedException("binary websocket messages are not supported");
+ }
+ }
+ }
+ catch (Exception e) {
+ Debug.WriteLine($"exception thrown while receiving websocket data: {e}");
+ throw;
+ }
+ }
+
+ private async Task _closeAsync(CancellationToken cancellationToken = default) {
+ if (clientWebSocket == null)
+ return;
+
+ // don't attempt to close the websocket if it is in a failed state
+ if (this.clientWebSocket.State != WebSocketState.Open &&
+ this.clientWebSocket.State != WebSocketState.CloseReceived &&
+ this.clientWebSocket.State != WebSocketState.CloseSent) {
+ Debug.WriteLine($"websocket {clientWebSocket.GetHashCode()} state = {this.clientWebSocket.State}");
+ return;
+ }
+
+ Debug.WriteLine($"closing websocket {clientWebSocket.GetHashCode()}");
+ await this.clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", cancellationToken).ConfigureAwait(false);
+ }
+
+#endregion
+
+#region IDisposable
+
+ private Task _disposed;
+ private object _disposedLocker = new object();
+ public void Dispose() {
+ // Async disposal as recommended by Stephen Cleary (https://blog.stephencleary.com/2013/03/async-oop-6-disposal.html)
+ lock (_disposedLocker) {
+ if (_disposed == null) _disposed = DisposeAsync();
+ }
+ }
+
+ private async Task DisposeAsync() {
+ Debug.WriteLine($"disposing websocket {clientWebSocket.GetHashCode()}...");
+ if (!_cancellationTokenSource.IsCancellationRequested)
+ _cancellationTokenSource.Cancel();
+ await _closeAsync().ConfigureAwait(false);
+ clientWebSocket?.Dispose();
+ _cancellationTokenSource.Dispose();
+ Debug.WriteLine($"websocket {clientWebSocket.GetHashCode()} disposed");
+ }
+#endregion
+ }
+}
diff --git a/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs b/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs
new file mode 100644
index 00000000..f3cb5854
--- /dev/null
+++ b/src/GraphQL.Client/Websocket/GraphQLHttpWebsocketHelpers.cs
@@ -0,0 +1,208 @@
+using System;
+using System.Diagnostics;
+using System.Net.WebSockets;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using System.Reactive.Threading.Tasks;
+using System.Threading;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+
+namespace GraphQL.Client.Http.Websocket {
+ public static class GraphQLHttpWebsocketHelpers {
+ internal static IObservable> CreateSubscriptionStream(
+ this GraphQLHttpWebSocket graphQlHttpWebSocket,
+ GraphQLRequest request,
+ GraphQLHttpClient client,
+ Action exceptionHandler = null,
+ CancellationToken cancellationToken = default) {
+ return Observable.Defer(() =>
+ Observable.Create>(async observer => {
+ await client.Options.PreprocessRequest(request, client);
+ var startRequest = new GraphQLWebSocketRequest {
+ Id = Guid.NewGuid().ToString("N"),
+ Type = GraphQLWebSocketMessageType.GQL_START,
+ Payload = request
+ };
+ var closeRequest = new GraphQLWebSocketRequest {
+ Id = startRequest.Id,
+ Type = GraphQLWebSocketMessageType.GQL_STOP
+ };
+ var initRequest = new GraphQLWebSocketRequest {
+ Id = startRequest.Id,
+ Type = GraphQLWebSocketMessageType.GQL_CONNECTION_INIT,
+ };
+
+ var observable = Observable.Create>(o =>
+ graphQlHttpWebSocket.ResponseStream
+ // ignore null values and messages for other requests
+ .Where(response => response != null && response.Id == startRequest.Id)
+ .Subscribe(response => {
+ // terminate the sequence when a 'complete' message is received
+ if (response.Type == GraphQLWebSocketMessageType.GQL_COMPLETE) {
+ Debug.WriteLine($"received 'complete' message on subscription {startRequest.Id}");
+ o.OnCompleted();
+ return;
+ }
+
+ // post the GraphQLResponse to the stream (even if a GraphQL error occurred)
+ Debug.WriteLine($"received payload on subscription {startRequest.Id}");
+ var typedResponse =
+ response.MessageBytes.DeserializeFromBytes>(client.Options);
+ o.OnNext(typedResponse.Payload);
+
+ // in case of a GraphQL error, terminate the sequence after the response has been posted
+ if (response.Type == GraphQLWebSocketMessageType.GQL_ERROR) {
+ Debug.WriteLine($"terminating subscription {startRequest.Id} because of a GraphQL error");
+ o.OnCompleted();
+ }
+ },
+ o.OnError,
+ o.OnCompleted)
+ );
+
+ try {
+ // initialize websocket (completes immediately if socket is already open)
+ await graphQlHttpWebSocket.InitializeWebSocket().ConfigureAwait(false);
+ }
+ catch (Exception e) {
+ // subscribe observer to failed observable
+ return Observable.Throw>(e).Subscribe(observer);
+ }
+
+ var disposable = new CompositeDisposable(
+ observable.Subscribe(observer),
+ Disposable.Create(async () => {
+ // only try to send close request on open websocket
+ if (graphQlHttpWebSocket.WebSocketState != WebSocketState.Open) return;
+
+ try {
+ Debug.WriteLine($"sending close message on subscription {startRequest.Id}");
+ await graphQlHttpWebSocket.SendWebSocketRequest(closeRequest).ConfigureAwait(false);
+ }
+ // do not break on disposing
+ catch (OperationCanceledException) { }
+ })
+ );
+
+ // send connection init
+ Debug.WriteLine($"sending connection init on subscription {startRequest.Id}");
+ try {
+ await graphQlHttpWebSocket.SendWebSocketRequest(initRequest).ConfigureAwait(false);
+ }
+ catch (Exception e) {
+ Console.WriteLine(e);
+ throw;
+ }
+
+ Debug.WriteLine($"sending initial message on subscription {startRequest.Id}");
+ // send subscription request
+ try {
+ await graphQlHttpWebSocket.SendWebSocketRequest(startRequest).ConfigureAwait(false);
+ }
+ catch (Exception e) {
+ Console.WriteLine(e);
+ throw;
+ }
+
+ return disposable;
+ }))
+ // complete sequence on OperationCanceledException, this is triggered by the cancellation token
+ .Catch, OperationCanceledException>(exception =>
+ Observable.Empty>())
+ // wrap results
+ .Select(response => new Tuple, Exception>(response, null))
+ // do exception handling
+ .Catch, Exception>, Exception>(e => {
+ try {
+ if (exceptionHandler == null) {
+ // if the external handler is not set, propagate all exceptions except WebSocketExceptions
+ // this will ensure that the client tries to re-establish subscriptions on connection loss
+ if (!(e is WebSocketException)) throw e;
+ }
+ else {
+ // exceptions thrown by the handler will propagate to OnError()
+ exceptionHandler?.Invoke(e);
+ }
+
+ // throw exception on the observable to be caught by Retry() or complete sequence if cancellation was requested
+ return cancellationToken.IsCancellationRequested
+ ? Observable.Empty, Exception>>()
+ : Observable.Throw, Exception>>(e);
+ }
+ catch (Exception exception) {
+ // wrap all other exceptions to be propagated behind retry
+ return Observable.Return(new Tuple, Exception>(null, exception));
+ }
+ })
+ // attempt to recreate the websocket for rethrown exceptions
+ .Retry()
+ // unwrap and push results or throw wrapped exceptions
+ .SelectMany(t => {
+ // if the result contains an exception, throw it on the observable
+ if (t.Item2 != null)
+ return Observable.Throw>(t.Item2);
+
+ return t.Item1 == null
+ ? Observable.Empty>()
+ : Observable.Return(t.Item1);
+ })
+ // transform to hot observable and auto-connect
+ .Publish().RefCount();
+ }
+
+ internal static Task> SendRequest(
+ this GraphQLHttpWebSocket graphQlHttpWebSocket,
+ GraphQLRequest request,
+ GraphQLHttpClient client,
+ CancellationToken cancellationToken = default) {
+ return Observable.Create>(async observer => {
+ await client.Options.PreprocessRequest(request, client);
+ var websocketRequest = new GraphQLWebSocketRequest {
+ Id = Guid.NewGuid().ToString("N"),
+ Type = GraphQLWebSocketMessageType.GQL_START,
+ Payload = request
+ };
+ var observable = graphQlHttpWebSocket.ResponseStream
+ .Where(response => response != null && response.Id == websocketRequest.Id)
+ .TakeUntil(response => response.Type == GraphQLWebSocketMessageType.GQL_COMPLETE)
+ .Select(response => {
+ Debug.WriteLine($"received response for request {websocketRequest.Id}"); ;
+ var typedResponse =
+ response.MessageBytes.DeserializeFromBytes>(client.Options);
+ return typedResponse.Payload;
+ });
+
+ try {
+ // intialize websocket (completes immediately if socket is already open)
+ await graphQlHttpWebSocket.InitializeWebSocket().ConfigureAwait(false);
+ }
+ catch (Exception e) {
+ // subscribe observer to failed observable
+ return Observable.Throw>(e).Subscribe(observer);
+ }
+
+ var disposable = new CompositeDisposable(
+ observable.Subscribe(observer)
+ );
+
+ Debug.WriteLine($"submitting request {websocketRequest.Id}");
+ // send request
+ try {
+ await graphQlHttpWebSocket.SendWebSocketRequest(websocketRequest).ConfigureAwait(false);
+ }
+ catch (Exception e) {
+ Console.WriteLine(e);
+ throw;
+ }
+
+ return disposable;
+ })
+ // complete sequence on OperationCanceledException, this is triggered by the cancellation token
+ .Catch, OperationCanceledException>(exception =>
+ Observable.Empty>())
+ .FirstOrDefaultAsync()
+ .ToTask(cancellationToken);
+ }
+ }
+}
diff --git a/src/GraphQL.Client/Websocket/GraphQLWebSocketMessageType.cs b/src/GraphQL.Client/Websocket/GraphQLWebSocketMessageType.cs
new file mode 100644
index 00000000..755a075b
--- /dev/null
+++ b/src/GraphQL.Client/Websocket/GraphQLWebSocketMessageType.cs
@@ -0,0 +1,87 @@
+namespace GraphQL.Client.Http.Websocket {
+ public static class GraphQLWebSocketMessageType {
+
+ ///
+ /// Client sends this message after plain websocket connection to start the communication with the server
+ /// The server will response only with GQL_CONNECTION_ACK + GQL_CONNECTION_KEEP_ALIVE(if used) or GQL_CONNECTION_ERROR
+ /// to this message.
+ /// payload: Object : optional parameters that the client specifies in connectionParams
+ ///
+ public const string GQL_CONNECTION_INIT = "connection_init";
+
+ ///
+ /// The server may responses with this message to the GQL_CONNECTION_INIT from client, indicates the server accepted
+ /// the connection.
+ ///
+ public const string GQL_CONNECTION_ACK = "connection_ack"; // Server -> Client
+
+ ///
+ /// The server may responses with this message to the GQL_CONNECTION_INIT from client, indicates the server rejected
+ /// the connection.
+ /// It server also respond with this message in case of a parsing errors of the message (which does not disconnect the
+ /// client, just ignore the message).
+ /// payload: Object: the server side error
+ ///
+ public const string GQL_CONNECTION_ERROR = "connection_error"; // Server -> Client
+
+ ///
+ /// Server message that should be sent right after each GQL_CONNECTION_ACK processed and then periodically to keep the
+ /// client connection alive.
+ /// The client starts to consider the keep alive message only upon the first received keep alive message from the
+ /// server.
+ ///
+ /// NOTE: This one here don't follow the standard due to connection optimization
+ ///
+ ///
+ public const string GQL_CONNECTION_KEEP_ALIVE = "ka"; // Server -> Client
+
+ ///
+ /// Client sends this message in order to stop a running GraphQL operation execution (for example: unsubscribe)
+ /// id: string : operation id
+ ///
+ public const string GQL_CONNECTION_TERMINATE = "connection_terminate"; // Client -> Server
+
+ ///
+ /// Client sends this message to execute GraphQL operation
+ /// id: string : The id of the GraphQL operation to start
+ /// payload: Object:
+ /// query: string : GraphQL operation as string or parsed GraphQL document node
+ /// variables?: Object : Object with GraphQL variables
+ /// operationName?: string : GraphQL operation name
+ ///
+ public const string GQL_START = "start";
+
+ ///
+ /// The server sends this message to transfer the GraphQL execution result from the server to the client, this message
+ /// is a response for GQL_START message.
+ /// For each GraphQL operation send with GQL_START, the server will respond with at least one GQL_DATA message.
+ /// id: string : ID of the operation that was successfully set up
+ /// payload: Object :
+ /// data: any: Execution result
+ /// errors?: Error[] : Array of resolvers errors
+ ///
+ public const string GQL_DATA = "data"; // Server -> Client
+
+ ///
+ /// Server sends this message upon a failing operation, before the GraphQL execution, usually due to GraphQL validation
+ /// errors (resolver errors are part of GQL_DATA message, and will be added as errors array)
+ /// payload: Error : payload with the error attributed to the operation failing on the server
+ /// id: string : operation ID of the operation that failed on the server
+ ///
+ public const string GQL_ERROR = "error"; // Server -> Client
+
+ ///
+ /// Server sends this message to indicate that a GraphQL operation is done, and no more data will arrive for the
+ /// specific operation.
+ /// id: string : operation ID of the operation that completed
+ ///
+ public const string GQL_COMPLETE = "complete"; // Server -> Client
+
+ ///
+ /// Client sends this message in order to stop a running GraphQL operation execution (for example: unsubscribe)
+ /// id: string : operation id
+ ///
+ public const string GQL_STOP = "stop"; // Client -> Server
+
+ }
+}
diff --git a/src/GraphQL.Client/Websocket/GraphQLWebSocketRequest.cs b/src/GraphQL.Client/Websocket/GraphQLWebSocketRequest.cs
new file mode 100644
index 00000000..01dcf173
--- /dev/null
+++ b/src/GraphQL.Client/Websocket/GraphQLWebSocketRequest.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace GraphQL.Client.Http.Websocket {
+
+ ///
+ /// A Subscription Request
+ ///
+ public class GraphQLWebSocketRequest : IEquatable {
+
+ ///
+ /// The Identifier of the Response
+ ///
+ public string Id { get; set; }
+
+ ///
+ /// The Type of the Request
+ ///
+ public string Type { get; set; }
+
+ ///
+ /// The payload of the websocket request
+ ///
+ public GraphQLRequest Payload { get; set; }
+
+ private TaskCompletionSource _tcs = new TaskCompletionSource();
+
+ ///
+ /// Task used to await the actual send operation and to convey potential exceptions
+ ///
+ ///
+ public Task SendTask() => _tcs.Task;
+
+ ///
+ /// gets called when the send operation for this request has completed sucessfully
+ ///
+ public void SendCompleted() => _tcs.SetResult(true);
+
+ ///
+ /// gets called when an exception occurs during the send operation
+ ///
+ ///
+ public void SendFailed(Exception e) => _tcs.SetException(e);
+
+ ///
+ /// gets called when the GraphQLHttpWebSocket has been disposed before the send operation for this request has started
+ ///
+ public void SendCanceled() => _tcs.SetCanceled();
+
+ ///
+ public override bool Equals(object obj) => this.Equals(obj as GraphQLWebSocketRequest);
+
+ ///
+ public bool Equals(GraphQLWebSocketRequest other) {
+ if (other == null) {
+ return false;
+ }
+ if (ReferenceEquals(this, other)) {
+ return true;
+ }
+ if (!Equals(this.Id, other.Id)) {
+ return false;
+ }
+ if (!Equals(this.Type, other.Type)) {
+ return false;
+ }
+ if (!Equals(this.Payload, other.Payload)) {
+ return false;
+ }
+ return true;
+ }
+
+ ///
+ public override int GetHashCode() {
+ var hashCode = 9958074;
+ hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(this.Id);
+ hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(this.Type);
+ hashCode = hashCode * -1521134295 + EqualityComparer
diff --git a/src/GraphQL.Primitives/GraphQLError.cs b/src/GraphQL.Primitives/GraphQLError.cs
index 6ccdd39c..c1b159a4 100644
--- a/src/GraphQL.Primitives/GraphQLError.cs
+++ b/src/GraphQL.Primitives/GraphQLError.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Runtime.Serialization;
namespace GraphQL {
@@ -11,23 +12,27 @@ public class GraphQLError : IEquatable {
///
/// The extensions of the error
- ///
- public IDictionary? Extensions { get; set; }
+ ///
+ [DataMember(Name = "extensions")]
+ public IDictionary? Extensions { get; set; }
///
/// The locations of the error
///
+ [DataMember(Name = "locations")]
public GraphQLLocation[]? Locations { get; set; }
///
/// The message of the error
///
+ [DataMember(Name = "message")]
public string Message { get; set; }
///
/// The Path of the error
///
- public dynamic[]? Path { get; set; }
+ [DataMember(Name = "path")]
+ public object[]? Path { get; set; }
///
/// Returns a value that indicates whether this instance is equal to a specified object
@@ -45,7 +50,7 @@ public override bool Equals(object? obj) =>
public bool Equals(GraphQLError? other) {
if (other == null) { return false; }
if (ReferenceEquals(this, other)) { return true; }
- if (!EqualityComparer?>.Default.Equals(this.Extensions, other.Extensions)) { return false; }
+ if (!EqualityComparer?>.Default.Equals(this.Extensions, other.Extensions)) { return false; }
{
if (this.Locations != null && other.Locations != null) {
if (!this.Locations.SequenceEqual(other.Locations)) { return false; }
@@ -70,7 +75,7 @@ public bool Equals(GraphQLError? other) {
public override int GetHashCode() {
var hashCode = 0;
if (this.Extensions != null) {
- hashCode = hashCode ^ EqualityComparer>.Default.GetHashCode(this.Extensions);
+ hashCode = hashCode ^ EqualityComparer?>.Default.GetHashCode(this.Extensions);
}
if (this.Locations != null) {
hashCode = hashCode ^ EqualityComparer.Default.GetHashCode(this.Locations);
diff --git a/src/GraphQL.Primitives/GraphQLRequest.cs b/src/GraphQL.Primitives/GraphQLRequest.cs
index ea0773e5..874b0352 100644
--- a/src/GraphQL.Primitives/GraphQLRequest.cs
+++ b/src/GraphQL.Primitives/GraphQLRequest.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Runtime.Serialization;
namespace GraphQL {
@@ -11,88 +12,55 @@ public class GraphQLRequest : IEquatable {
///
/// The Query
///
+ ///
+ [DataMember(Name = "query")]
public string Query { get; set; }
///
/// The name of the Operation
///
+ [DataMember(Name = "operationName")]
public string? OperationName { get; set; }
///
- /// Returns a value that indicates whether this instance is equal to a specified object
+ /// Represents the request variables
///
- /// The object to compare with this instance
- /// true if obj is an instance of and equals the value of the instance; otherwise, false
- public override bool Equals(object? obj) => this.Equals(obj as GraphQLRequest);
+ [DataMember(Name = "variables")]
+ public virtual object? Variables { get; set; }
- ///
- /// Returns a value that indicates whether this instance is equal to a specified object
- ///
- /// The object to compare with this instance
- /// true if obj is an instance of and equals the value of the instance; otherwise, false
- public bool Equals(GraphQLRequest? other) {
- if (other == null) { return false; }
- if (ReferenceEquals(this, other)) { return true; }
- if (!EqualityComparer.Default.Equals(this.Query, other.Query)) { return false; }
- if (!EqualityComparer.Default.Equals(this.OperationName, other.OperationName)) { return false; }
- return true;
- }
- ///
- ///
- ///
- public override int GetHashCode() {
- unchecked {
- var hashCode = EqualityComparer.Default.GetHashCode(this.Query);
- hashCode = (hashCode * 397) ^ EqualityComparer.Default.GetHashCode(this.OperationName);
- return hashCode;
- }
+ public GraphQLRequest() {
}
- ///
- /// Tests whether two specified instances are equivalent
- ///
- /// The instance that is to the left of the equality operator
- /// The instance that is to the right of the equality operator
- /// true if left and right are equal; otherwise, false
- public static bool operator ==(GraphQLRequest? left, GraphQLRequest? right) => EqualityComparer.Default.Equals(left, right);
-
- ///
- /// Tests whether two specified instances are not equal
- ///
- /// The instance that is to the left of the not equal operator
- /// The instance that is to the right of the not equal operator
- /// true if left and right are unequal; otherwise, false
- public static bool operator !=(GraphQLRequest? left, GraphQLRequest? right) => !(left == right);
-
- }
-
- public class GraphQLRequest : GraphQLRequest, IEquatable?> {
-
- ///
- /// Represents the variables sended
- ///
- public T Variables { get; set; }
+ public GraphQLRequest(string query, object? variables = null, string? operationName = null) {
+ Query = query;
+ Variables = variables;
+ OperationName = operationName;
+ }
///
/// Returns a value that indicates whether this instance is equal to a specified object
///
/// The object to compare with this instance
/// true if obj is an instance of and equals the value of the instance; otherwise, false
- public override bool Equals(object? obj) => this.Equals(obj as GraphQLRequest);
+ public override bool Equals(object? obj) {
+ if (ReferenceEquals(null, obj)) return false;
+ if (ReferenceEquals(this, obj)) return true;
+ if (obj.GetType() != this.GetType()) return false;
+ return Equals((GraphQLRequest)obj);
+ }
///
/// Returns a value that indicates whether this instance is equal to a specified object
///
/// The object to compare with this instance
/// true if obj is an instance of and equals the value of the instance; otherwise, false
- public bool Equals(GraphQLRequest? other) {
- if (other == null) { return false; }
- if (ReferenceEquals(this, other)) { return true; }
- if (!EqualityComparer.Default.Equals(this.Query, other.Query)) { return false; }
- if (!EqualityComparer.Default.Equals(this.OperationName, other.OperationName)) { return false; }
- if (!EqualityComparer.Default.Equals(this.Variables, other.Variables)) { return false; }
- return true;
+ public virtual bool Equals(GraphQLRequest? other) {
+ if (ReferenceEquals(null, other)) return false;
+ if (ReferenceEquals(this, other)) return true;
+ return Query == other.Query
+ && OperationName == other.OperationName
+ && EqualityComparer
diff --git a/tests/GraphQL.Client.Http.Tests/BaseTest.cs b/tests/GraphQL.Client.Http.Tests/BaseTest.cs
deleted file mode 100644
index ba47cdea..00000000
--- a/tests/GraphQL.Client.Http.Tests/BaseTest.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System;
-using Microsoft.AspNetCore.TestHost;
-
-namespace GraphQL.Client.Http.Tests {
-
- public abstract class BaseTest : IDisposable {
-
- private readonly TestServer testServer = new TestServer(Server.Test.Program.CreateHostBuilder());
-
- public void Dispose() {
- this.testServer.Dispose();
- }
-
- }
-
-}
diff --git a/tests/GraphQL.Client.Http.Tests/GraphQL.Client.Http.Tests.csproj b/tests/GraphQL.Client.Http.Tests/GraphQL.Client.Http.Tests.csproj
deleted file mode 100644
index 3114fa2d..00000000
--- a/tests/GraphQL.Client.Http.Tests/GraphQL.Client.Http.Tests.csproj
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
- netcoreapp3.1
-
-
-
-
-
-
-
-
-
-
-
diff --git a/tests/GraphQL.Client.Tests/GraphQL.Client.Tests.csproj b/tests/GraphQL.Client.Tests/GraphQL.Client.Tests.csproj
deleted file mode 100644
index 11fa8112..00000000
--- a/tests/GraphQL.Client.Tests/GraphQL.Client.Tests.csproj
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
- netcoreapp3.1
-
-
-
-
-
-
-
diff --git a/tests/GraphQL.Client.Tests/GraphQLRequestTest.cs b/tests/GraphQL.Client.Tests/GraphQLRequestTest.cs
deleted file mode 100644
index c63a79bc..00000000
--- a/tests/GraphQL.Client.Tests/GraphQLRequestTest.cs
+++ /dev/null
@@ -1,234 +0,0 @@
-using Xunit;
-
-namespace GraphQL.Client.Tests {
-
- public class GraphQLRequestTest {
-
- [Fact]
- public void ConstructorFact() {
- var graphQLRequest = new GraphQLRequest { Query = "{hero{name}}" };
- Assert.NotNull(graphQLRequest.Query);
- Assert.Null(graphQLRequest.OperationName);
- Assert.Null(graphQLRequest.Variables);
- }
-
- [Fact]
- public void ConstructorExtendedFact() {
- var graphQLRequest = new GraphQLRequest {
- Query = "{hero{name}}",
- OperationName = "operationName",
- Variables = new {
- varName = "varValue"
- }
- };
- Assert.NotNull(graphQLRequest.Query);
- Assert.NotNull(graphQLRequest.OperationName);
- Assert.NotNull(graphQLRequest.Variables);
- }
-
- [Fact]
- public void Equality1Fact() {
- var graphQLRequest = new GraphQLRequest { Query = "{hero{name}}" };
- Assert.Equal(graphQLRequest, graphQLRequest);
- }
-
- [Fact]
- public void Equality2Fact() {
- var graphQLRequest1 = new GraphQLRequest { Query = "{hero{name}}" };
- var graphQLRequest2 = new GraphQLRequest { Query = "{hero{name}}" };
- Assert.Equal(graphQLRequest1, graphQLRequest2);
- }
-
- [Fact]
- public void Equality3Fact() {
- var graphQLRequest1 = new GraphQLRequest {
- Query = "{hero{name}}",
- OperationName = "operationName",
- Variables = new {
- varName = "varValue"
- }
- };
- var graphQLRequest2 = new GraphQLRequest {
- Query = "{hero{name}}",
- OperationName = "operationName",
- Variables = new {
- varName = "varValue"
- }
- };
- Assert.Equal(graphQLRequest1, graphQLRequest2);
- }
-
- [Fact]
- public void EqualityOperatorFact() {
- var graphQLRequest1 = new GraphQLRequest {
- Query = "{hero{name}}",
- OperationName = "operationName",
- Variables = new {
- varName = "varValue"
- }
- };
- var graphQLRequest2 = new GraphQLRequest {
- Query = "{hero{name}}",
- OperationName = "operationName",
- Variables = new {
- varName = "varValue"
- }
- };
- Assert.True(graphQLRequest1 == graphQLRequest2);
- }
-
- [Fact]
- public void InEquality1Fact() {
- var graphQLRequest1 = new GraphQLRequest { Query = "{hero{name1}}" };
- var graphQLRequest2 = new GraphQLRequest { Query = "{hero{name2}}" };
- Assert.NotEqual(graphQLRequest1, graphQLRequest2);
- }
-
- [Fact]
- public void InEquality2Fact() {
- var graphQLRequest1 = new GraphQLRequest {
- Query = "{hero{name}}",
- OperationName = "operationName",
- Variables = new {
- varName = "varValue1"
- }
- };
- var graphQLRequest2 = new GraphQLRequest {
- Query = "{hero{name}}",
- OperationName = "operationName",
- Variables = new {
- varName = "varValue2"
- }
- };
- Assert.NotEqual(graphQLRequest1, graphQLRequest2);
- }
-
- [Fact]
- public void InEqualityOperatorFact() {
- var graphQLRequest1 = new GraphQLRequest { Query = "{hero{name1}}" };
- var graphQLRequest2 = new GraphQLRequest { Query = "{hero{name2}}" };
- Assert.True(graphQLRequest1 != graphQLRequest2);
- }
-
- [Fact]
- public void GetHashCode1Fact() {
- var graphQLRequest1 = new GraphQLRequest { Query = "{hero{name}}" };
- var graphQLRequest2 = new GraphQLRequest { Query = "{hero{name}}" };
- Assert.True(graphQLRequest1.GetHashCode() == graphQLRequest2.GetHashCode());
- }
-
- [Fact]
- public void GetHashCode2Fact() {
- var graphQLRequest1 = new GraphQLRequest {
- Query = "{hero{name}}",
- OperationName = "operationName",
- Variables = new {
- varName = "varValue"
- }
- };
- var graphQLRequest2 = new GraphQLRequest {
- Query = "{hero{name}}",
- OperationName = "operationName",
- Variables = new {
- varName = "varValue"
- }
- };
- Assert.True(graphQLRequest1.GetHashCode() == graphQLRequest2.GetHashCode());
- }
-
- [Fact]
- public void PropertyQueryGetFact() {
- var graphQLRequest = new GraphQLRequest {
- Query = "{hero{name}}",
- OperationName = "operationName",
- Variables = new {
- varName = "varValue"
- }
- };
- Assert.Equal("{hero{name}}", graphQLRequest.Query);
- }
-
- [Fact]
- public void PropertyQuerySetFact() {
- var graphQLRequest = new GraphQLRequest {
- Query = "{hero{name1}}",
- OperationName = "operationName",
- Variables = new {
- varName = "varValue"
- }
- };
- graphQLRequest.Query = "{hero{name2}}";
- Assert.Equal("{hero{name2}}", graphQLRequest.Query);
- }
-
- [Fact]
- public void PropertyOperationNameGetFact() {
- var graphQLRequest = new GraphQLRequest {
- Query = "{hero{name}}",
- OperationName = "operationName",
- Variables = new {
- varName = "varValue"
- }
- };
- Assert.Equal("operationName", graphQLRequest.OperationName);
- }
-
- [Fact]
- public void PropertyOperationNameNullGetFact() {
- var graphQLRequest = new GraphQLRequest { Query = "{hero{name}}" };
- Assert.Null(graphQLRequest.OperationName);
- }
-
- [Fact]
- public void PropertyOperationNameSetFact() {
- var graphQLRequest = new GraphQLRequest {
- Query = "{hero{name}}",
- OperationName = "operationName1",
- Variables = new {
- varName = "varValue"
- }
- };
- graphQLRequest.OperationName = "operationName2";
- Assert.Equal("operationName2", graphQLRequest.OperationName);
- }
-
- [Fact]
- public void PropertyVariableGetFact() {
- var graphQLRequest = new GraphQLRequest {
- Query = "{hero{name}}",
- OperationName = "operationName",
- Variables = new {
- varName = "varValue"
- }
- };
- Assert.Equal(new {
- varName = "varValue"
- }, graphQLRequest.Variables);
- }
-
- [Fact]
- public void PropertyVariableNullGetFact() {
- var graphQLRequest = new GraphQLRequest { Query = "{hero{name}}" };
- Assert.Null(graphQLRequest.Variables);
- }
-
- [Fact]
- public void PropertyVariableSetFact() {
- var graphQLRequest = new GraphQLRequest {
- Query = "{hero{name}}",
- OperationName = "operationName",
- Variables = new {
- varName = "varValue1"
- }
- };
- graphQLRequest.Variables = new {
- varName = "varValue2"
- };
- Assert.Equal(new {
- varName = "varValue2"
- }, graphQLRequest.Variables);
- }
-
- }
-
-}
diff --git a/tests/GraphQL.Client.Tests/Model/Person.cs b/tests/GraphQL.Client.Tests/Model/Person.cs
deleted file mode 100644
index 23b86a43..00000000
--- a/tests/GraphQL.Client.Tests/Model/Person.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-namespace GraphQL.Client.Tests.Model {
-
- public class Person {
-
- public string[] AppearsIn { get; set; }
-
- public Person[] Friends { get; set; }
-
- public double Height { get; set; }
-
- public string Name { get; set; }
-
- public string PrimaryFunction { get; set; }
-
- }
-
-}
diff --git a/tests/GraphQL.Integration.Tests/Extensions/GraphQLClientTestExtensions.cs b/tests/GraphQL.Integration.Tests/Extensions/GraphQLClientTestExtensions.cs
new file mode 100644
index 00000000..f698b247
--- /dev/null
+++ b/tests/GraphQL.Integration.Tests/Extensions/GraphQLClientTestExtensions.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Threading.Tasks;
+using GraphQL.Client;
+using GraphQL.Client.Http;
+
+namespace GraphQL.Integration.Tests.Extensions {
+ public static class GraphQLClientTestExtensions {
+ public static Task> AddMessageAsync(this GraphQLHttpClient client, string message) {
+ var graphQLRequest = new GraphQLRequest(
+ @"mutation($input: MessageInputType){
+ addMessage(message: $input){
+ content
+ }
+ }",
+ new {
+ input = new {
+ fromId = "2",
+ content = message,
+ sentAt = DateTime.Now
+ }
+ });
+ return client.SendMutationAsync(graphQLRequest);
+ }
+
+ public static Task> JoinDeveloperUser(this GraphQLHttpClient client) {
+ var graphQLRequest = new GraphQLRequest(@"
+ mutation($userId: String){
+ join(userId: $userId){
+ displayName
+ id
+ }
+ }",
+ new {
+ userId = "1"
+ });
+ return client.SendMutationAsync(graphQLRequest);
+ }
+
+ }
+
+
+ public class AddMessageMutationResult {
+ public AddMessageContent AddMessage { get; set; }
+ public class AddMessageContent {
+ public string Content { get; set; }
+ }
+ }
+
+ public class JoinDeveloperMutationResult {
+ public JoinContent Join { get; set; }
+ public class JoinContent {
+ public string DisplayName { get; set; }
+ public string Id { get; set; }
+ }
+ }
+}
diff --git a/tests/GraphQL.Integration.Tests/Extensions/WebApplicationFactoryExtensions.cs b/tests/GraphQL.Integration.Tests/Extensions/WebApplicationFactoryExtensions.cs
new file mode 100644
index 00000000..de4848fc
--- /dev/null
+++ b/tests/GraphQL.Integration.Tests/Extensions/WebApplicationFactoryExtensions.cs
@@ -0,0 +1,16 @@
+using System;
+using GraphQL.Client.Http;
+using Microsoft.AspNetCore.Mvc.Testing;
+
+namespace GraphQL.Integration.Tests.Extensions {
+ public static class WebApplicationFactoryExtensions {
+ public static GraphQLHttpClient CreateGraphQlHttpClient(
+ this WebApplicationFactory factory, string graphQlSchemaUrl, string urlScheme = "http://") where TEntryPoint : class {
+ var httpClient = factory.CreateClient();
+ var uriBuilder = new UriBuilder(httpClient.BaseAddress);
+ uriBuilder.Path = graphQlSchemaUrl;
+ uriBuilder.Scheme = urlScheme;
+ return httpClient.AsGraphQLClient(uriBuilder.Uri);
+ }
+ }
+}
diff --git a/tests/GraphQL.Integration.Tests/ExtensionsTest.cs b/tests/GraphQL.Integration.Tests/ExtensionsTest.cs
new file mode 100644
index 00000000..fff1b6e4
--- /dev/null
+++ b/tests/GraphQL.Integration.Tests/ExtensionsTest.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Text.Json;
+using FluentAssertions;
+using GraphQL.Client.Abstractions;
+using GraphQL.Integration.Tests.Helpers;
+using IntegrationTestServer;
+using IntegrationTestServer.ChatSchema;
+using Newtonsoft.Json;
+using Xunit;
+
+namespace GraphQL.Integration.Tests {
+ public class ExtensionsTest {
+ private static TestServerSetup SetupTest(bool requestsViaWebsocket = false) =>
+ WebHostHelpers.SetupTest(requestsViaWebsocket);
+
+ [Fact]
+ public async void CanDeserializeExtensions() {
+
+ using var setup = SetupTest();
+ var response = await setup.Client.SendQueryAsync(new GraphQLRequest("query { extensionsTest }"),
+ () => new {extensionsTest = ""})
+ .ConfigureAwait(false);
+
+ response.Errors.Should().NotBeNull();
+ response.Errors.Should().ContainSingle();
+ response.Errors[0].Extensions.Should().NotBeNull();
+ response.Errors[0].Extensions.Should().ContainKey("data");
+
+ foreach (var item in ChatQuery.TestExtensions) {
+
+
+ }
+ }
+
+ [Fact]
+ public async void DontNeedToUseCamelCaseNamingStrategy() {
+
+ using var setup = SetupTest();
+ setup.Client.Options.JsonSerializerSettings = new JsonSerializerSettings();
+
+ const string message = "some random testing message";
+ var graphQLRequest = new GraphQLRequest(
+ @"mutation($input: MessageInputType){
+ addMessage(message: $input){
+ content
+ }
+ }",
+ new {
+ input = new {
+ fromId = "2",
+ content = message,
+ sentAt = DateTime.Now
+ }
+ });
+ var response = await setup.Client.SendMutationAsync(graphQLRequest, () => new { addMessage = new { content = "" } });
+
+ Assert.Equal(message, response.Data.addMessage.content);
+ }
+ }
+}
diff --git a/tests/GraphQL.Integration.Tests/GraphQL.Integration.Tests.csproj b/tests/GraphQL.Integration.Tests/GraphQL.Integration.Tests.csproj
new file mode 100644
index 00000000..b1731cc1
--- /dev/null
+++ b/tests/GraphQL.Integration.Tests/GraphQL.Integration.Tests.csproj
@@ -0,0 +1,28 @@
+
+
+
+ netcoreapp3.1
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
+
+
+
+
+
+
diff --git a/tests/GraphQL.Integration.Tests/Helpers/CallbackTester.cs b/tests/GraphQL.Integration.Tests/Helpers/CallbackTester.cs
new file mode 100644
index 00000000..e0d46b5f
--- /dev/null
+++ b/tests/GraphQL.Integration.Tests/Helpers/CallbackTester.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Threading;
+using Xunit;
+
+namespace GraphQL.Integration.Tests.Helpers {
+ public class CallbackTester {
+ private ManualResetEventSlim _callbackInvoked { get; } = new ManualResetEventSlim();
+
+ ///
+ /// The timeout for . Defaults to 1 s
+ ///
+ public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(1);
+
+ ///
+ /// Indicates that an update has been received since the last
+ ///
+ public bool CallbackInvoked => _callbackInvoked.IsSet;
+ ///
+ /// The last payload which was received.
+ ///
+ public T LastPayload { get; private set; }
+
+ public void Callback(T param) {
+ LastPayload = param;
+ _callbackInvoked.Set();
+ }
+
+ ///
+ /// Asserts that a new update has been pushed to the within the configured since the last .
+ /// If supplied, the action is executed on the submitted payload.
+ ///
+ /// action to assert the contents of the payload
+ public void CallbackShouldHaveBeenInvoked(Action assertPayload = null, TimeSpan? timeout = null) {
+ try {
+ if (!_callbackInvoked.Wait(timeout ?? Timeout))
+ Assert.True(false, $"callback not invoked within {(timeout ?? Timeout).TotalSeconds} s!");
+
+ assertPayload?.Invoke(LastPayload);
+ }
+ finally {
+ Reset();
+ }
+ }
+
+ ///
+ /// Asserts that no new update has been pushed within the given since the last
+ ///
+ /// the time in ms in which no new update must be pushed to the . defaults to 100
+ public void CallbackShouldNotHaveBeenInvoked(TimeSpan? timeout = null) {
+ if (!timeout.HasValue) timeout = TimeSpan.FromMilliseconds(100);
+ try {
+ if (_callbackInvoked.Wait(timeout.Value))
+ Assert.True(false, "callback was inadvertently invoked pushed!");
+ }
+ finally {
+ Reset();
+ }
+ }
+
+ ///
+ /// Resets the tester class. Should be called before triggering the potential update
+ ///
+ public void Reset() {
+ LastPayload = default(T);
+ _callbackInvoked.Reset();
+ }
+ }
+}
diff --git a/tests/GraphQL.Integration.Tests/Helpers/NetworkHelpers.cs b/tests/GraphQL.Integration.Tests/Helpers/NetworkHelpers.cs
new file mode 100644
index 00000000..6995ee54
--- /dev/null
+++ b/tests/GraphQL.Integration.Tests/Helpers/NetworkHelpers.cs
@@ -0,0 +1,14 @@
+using System.Net;
+using System.Net.Sockets;
+
+namespace GraphQL.Integration.Tests.Helpers {
+ public static class NetworkHelpers {
+ public static int GetFreeTcpPortNumber() {
+ var l = new TcpListener(IPAddress.Loopback, 0);
+ l.Start();
+ var port = ((IPEndPoint)l.LocalEndpoint).Port;
+ l.Stop();
+ return port;
+ }
+ }
+}
diff --git a/tests/GraphQL.Integration.Tests/Helpers/ObservableTester.cs b/tests/GraphQL.Integration.Tests/Helpers/ObservableTester.cs
new file mode 100644
index 00000000..a650237b
--- /dev/null
+++ b/tests/GraphQL.Integration.Tests/Helpers/ObservableTester.cs
@@ -0,0 +1,129 @@
+using System;
+using System.Threading;
+using Xunit;
+
+namespace GraphQL.Integration.Tests.Helpers {
+ public class ObservableTester : IDisposable {
+ private readonly IDisposable _subscription;
+ private ManualResetEventSlim _updateReceived { get; } = new ManualResetEventSlim();
+ private ManualResetEventSlim _completed { get; } = new ManualResetEventSlim();
+ private ManualResetEventSlim _error { get; } = new ManualResetEventSlim();
+
+ ///
+ /// The timeout for . Defaults to 1 s
+ ///
+ public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(3);
+
+ ///
+ /// Indicates that an update has been received since the last
+ ///
+ public bool UpdateReceived => _updateReceived.IsSet;
+ ///
+ /// The last payload which was received.
+ ///
+ public T LastPayload { get; private set; }
+
+ public Exception Error { get; private set; }
+
+ ///
+ /// Creates a new which subscribes to the supplied
+ ///
+ /// the under test
+ public ObservableTester(IObservable observable) {
+ _subscription = observable.Subscribe(
+ obj => {
+ LastPayload = obj;
+ _updateReceived.Set();
+ },
+ ex => {
+ Error = ex;
+ _error.Set();
+ },
+ () => _completed.Set()
+ );
+ }
+
+ ///
+ /// Asserts that a new update has been pushed to the within the configured since the last .
+ /// If supplied, the action is executed on the submitted payload.
+ ///
+ /// action to assert the contents of the payload
+ public void ShouldHaveReceivedUpdate(Action assertPayload = null, TimeSpan? timeout = null) {
+ try {
+ if (!_updateReceived.Wait(timeout ?? Timeout))
+ Assert.True(false, $"no update received within {(timeout ?? Timeout).TotalSeconds} s!");
+
+ assertPayload?.Invoke(LastPayload);
+ }
+ finally {
+ _reset();
+ }
+ }
+
+ ///
+ /// Asserts that no new update has been pushed within the given since the last
+ ///
+ /// the time in ms in which no new update must be pushed to the . defaults to 100
+ public void ShouldNotHaveReceivedUpdate(TimeSpan? timeout = null) {
+ if (!timeout.HasValue) timeout = TimeSpan.FromMilliseconds(100);
+ try {
+ if (_updateReceived.Wait(timeout.Value))
+ Assert.True(false, "update was inadvertently pushed!");
+ }
+ finally {
+ _reset();
+ }
+ }
+
+ ///
+ /// Asserts that the subscription has completed within the configured since the last
+ ///
+ public void ShouldHaveCompleted(TimeSpan? timeout = null) {
+ try {
+ if (!_completed.Wait(timeout ?? Timeout))
+ Assert.True(false, $"subscription did not complete within {(timeout ?? Timeout).TotalSeconds} s!");
+ }
+ finally {
+ _reset();
+ }
+ }
+
+ ///
+ /// Asserts that the subscription has completed within the configured since the last
+ ///
+ public void ShouldHaveThrownError(Action assertError = null, TimeSpan? timeout = null) {
+ try {
+ if (!_error.Wait(timeout ?? Timeout))
+ Assert.True(false, $"subscription did not throw an error within {(timeout ?? Timeout).TotalSeconds} s!");
+
+ assertError?.Invoke(Error);
+ }
+ finally {
+ _reset();
+ }
+ }
+
+ ///
+ /// Resets the tester class. Should be called before triggering the potential update
+ ///
+ private void _reset() {
+ //if (_completed.IsSet)
+ // throw new InvalidOperationException(
+ // "the subscription sequence has completed. this tester instance cannot be reused");
+
+ LastPayload = default(T);
+ _updateReceived.Reset();
+ }
+
+ ///
+ public void Dispose() {
+ _subscription?.Dispose();
+ }
+ }
+
+ public static class ObservableExtensions {
+ public static ObservableTester SubscribeTester(this IObservable observable) {
+ return new ObservableTester(observable);
+ }
+ }
+}
diff --git a/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs b/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs
new file mode 100644
index 00000000..8c32f558
--- /dev/null
+++ b/tests/GraphQL.Integration.Tests/Helpers/WebHostHelpers.cs
@@ -0,0 +1,60 @@
+using System;
+using GraphQL.Client;
+using GraphQL.Client.Http;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace GraphQL.Integration.Tests.Helpers
+{
+ public static class WebHostHelpers
+ {
+ public static IWebHost CreateServer(int port) where TStartup : class
+ {
+ var configBuilder = new ConfigurationBuilder();
+ configBuilder.AddInMemoryCollection();
+ var config = configBuilder.Build();
+ config["server.urls"] = $"http://localhost:{port}";
+
+ var host = new WebHostBuilder()
+ .ConfigureLogging((ctx, logging) => {
+ logging.AddDebug();
+ })
+ .UseConfiguration(config)
+ .UseKestrel()
+ .UseStartup()
+ .Build();
+
+ host.Start();
+
+ return host;
+ }
+
+
+ public static GraphQLHttpClient GetGraphQLClient(int port, bool requestsViaWebsocket = false)
+ => new GraphQLHttpClient(new GraphQLHttpClientOptions {
+ EndPoint = new Uri($"http://localhost:{port}/graphql"),
+ UseWebSocketForQueriesAndMutations = requestsViaWebsocket
+ });
+
+ public static TestServerSetup SetupTest(bool requestsViaWebsocket = false) where TStartup : class
+ {
+ var port = NetworkHelpers.GetFreeTcpPortNumber();
+ return new TestServerSetup {
+ Server = CreateServer(port),
+ Client = GetGraphQLClient(port)
+ };
+ }
+ }
+
+ public class TestServerSetup : IDisposable
+ {
+ public IWebHost Server { get; set; }
+ public GraphQLHttpClient Client { get; set; }
+ public void Dispose()
+ {
+ Server?.Dispose();
+ Client?.Dispose();
+ }
+ }
+}
diff --git a/tests/GraphQL.Integration.Tests/Properties/launchSettings.json b/tests/GraphQL.Integration.Tests/Properties/launchSettings.json
new file mode 100644
index 00000000..f8fea71b
--- /dev/null
+++ b/tests/GraphQL.Integration.Tests/Properties/launchSettings.json
@@ -0,0 +1,27 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:51432/",
+ "sslPort": 0
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "GraphQL.Integration.Tests": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "http://localhost:51433/"
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/GraphQL.Integration.Tests/QueryAndMutationTests.cs b/tests/GraphQL.Integration.Tests/QueryAndMutationTests.cs
new file mode 100644
index 00000000..d897884f
--- /dev/null
+++ b/tests/GraphQL.Integration.Tests/QueryAndMutationTests.cs
@@ -0,0 +1,152 @@
+using System.Net.Http;
+using System.Text.Json;
+using GraphQL.Client.Abstractions;
+using GraphQL.Client.Http;
+using GraphQL.Integration.Tests.Helpers;
+using GraphQL.Integration.Tests.TestData;
+using IntegrationTestServer;
+using Newtonsoft.Json.Linq;
+using Xunit;
+
+namespace GraphQL.Integration.Tests {
+ public class QueryAndMutationTests {
+
+ private static TestServerSetup SetupTest(bool requestsViaWebsocket = false) => WebHostHelpers.SetupTest(requestsViaWebsocket);
+
+ [Theory]
+ [ClassData(typeof(StarWarsHumans))]
+ public async void QueryTheory(int id, string name) {
+ var graphQLRequest = new GraphQLRequest($"{{ human(id: \"{id}\") {{ name }} }}");
+
+ using (var setup = SetupTest()) {
+ var response = await setup.Client.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty }})
+ .ConfigureAwait(false);
+
+ Assert.Null(response.Errors);
+ Assert.Equal(name, response.Data.Human.Name);
+ }
+ }
+
+ [Theory]
+ [ClassData(typeof(StarWarsHumans))]
+ public async void QueryWithDynamicReturnTypeTheory(int id, string name) {
+ var graphQLRequest = new GraphQLRequest($"{{ human(id: \"{id}\") {{ name }} }}");
+
+ using (var setup = SetupTest()) {
+ var response = await setup.Client.SendQueryAsync(graphQLRequest)
+ .ConfigureAwait(false);
+
+ Assert.Null(response.Errors);
+ Assert.Equal(name, response.Data.human.name.ToString());
+ }
+ }
+
+ [Theory]
+ [ClassData(typeof(StarWarsHumans))]
+ public async void QueryWitVarsTheory(int id, string name) {
+ var graphQLRequest = new GraphQLRequest(@"
+ query Human($id: String!){
+ human(id: $id) {
+ name
+ }
+ }",
+ new {id = id.ToString()});
+
+ using (var setup = SetupTest()) {
+ var response = await setup.Client.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } })
+ .ConfigureAwait(false);
+
+ Assert.Null(response.Errors);
+ Assert.Equal(name, response.Data.Human.Name);
+ }
+ }
+
+ [Theory]
+ [ClassData(typeof(StarWarsHumans))]
+ public async void QueryWitVarsAndOperationNameTheory(int id, string name) {
+ var graphQLRequest = new GraphQLRequest(@"
+ query Human($id: String!){
+ human(id: $id) {
+ name
+ }
+ }
+
+ query Droid($id: String!) {
+ droid(id: $id) {
+ name
+ }
+ }",
+ new { id = id.ToString() },
+ "Human");
+
+ using (var setup = SetupTest()) {
+ var response = await setup.Client.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } })
+ .ConfigureAwait(false);
+
+ Assert.Null(response.Errors);
+ Assert.Equal(name, response.Data.Human.Name);
+ }
+ }
+
+ [Fact]
+ public async void SendMutationFact() {
+ var mutationRequest = new GraphQLRequest(@"
+ mutation CreateHuman($human: HumanInput!) {
+ createHuman(human: $human) {
+ id
+ name
+ homePlanet
+ }
+ }",
+ new { human = new { name = "Han Solo", homePlanet = "Corellia"}});
+
+ var queryRequest = new GraphQLRequest(@"
+ query Human($id: String!){
+ human(id: $id) {
+ name
+ }
+ }");
+
+ using (var setup = SetupTest()) {
+ var mutationResponse = await setup.Client.SendMutationAsync(mutationRequest, () => new {
+ createHuman = new {
+ Id = "",
+ Name = "",
+ HomePlanet = ""
+ }
+ })
+ .ConfigureAwait(false);
+
+ Assert.Null(mutationResponse.Errors);
+ Assert.Equal("Han Solo", mutationResponse.Data.createHuman.Name);
+ Assert.Equal("Corellia", mutationResponse.Data.createHuman.HomePlanet);
+
+ queryRequest.Variables = new {id = mutationResponse.Data.createHuman.Id};
+ var queryResponse = await setup.Client.SendQueryAsync(queryRequest, () => new { Human = new { Name = string.Empty } })
+ .ConfigureAwait(false);
+
+ Assert.Null(queryResponse.Errors);
+ Assert.Equal("Han Solo", queryResponse.Data.Human.Name);
+ }
+ }
+
+ [Fact]
+ public async void PreprocessHttpRequestMessageIsCalled() {
+ var callbackTester = new CallbackTester();
+ var graphQLRequest = new GraphQLHttpRequest($"{{ human(id: \"1\") {{ name }} }}") {
+ PreprocessHttpRequestMessage = callbackTester.Callback
+ };
+
+ using (var setup = SetupTest()) {
+ var defaultHeaders = setup.Client.HttpClient.DefaultRequestHeaders;
+ var response = await setup.Client.SendQueryAsync(graphQLRequest, () => new { Human = new { Name = string.Empty } })
+ .ConfigureAwait(false);
+ callbackTester.CallbackShouldHaveBeenInvoked(message => {
+ Assert.Equal(defaultHeaders, message.Headers);
+ });
+ Assert.Null(response.Errors);
+ Assert.Equal("Luke", response.Data.Human.Name);
+ }
+ }
+ }
+}
diff --git a/tests/GraphQL.Integration.Tests/TestData/StarWarsHumans.cs b/tests/GraphQL.Integration.Tests/TestData/StarWarsHumans.cs
new file mode 100644
index 00000000..99312c8c
--- /dev/null
+++ b/tests/GraphQL.Integration.Tests/TestData/StarWarsHumans.cs
@@ -0,0 +1,15 @@
+using System.Collections;
+using System.Collections.Generic;
+
+namespace GraphQL.Integration.Tests.TestData {
+ public class StarWarsHumans: IEnumerable