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 [![NuGet](https://img.shields.io/nuget/v/GraphQL.Client.svg)](https://www.nuget.org/packages/GraphQL.Client) -[![MyGet](https://img.shields.io/myget/graphql-dotnet/v/GraphQL.Client.svg)](https://www.myget.org/feed/graphql-dotnet/package/nuget/GraphQL.Client) +[![NuGet](https://img.shields.io/nuget/vpre/GraphQL.Client.svg)](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.Default.GetHashCode(this.Payload); + return hashCode; + } + + /// + public static bool operator ==(GraphQLWebSocketRequest request1, GraphQLWebSocketRequest request2) => EqualityComparer.Default.Equals(request1, request2); + + /// + public static bool operator !=(GraphQLWebSocketRequest request1, GraphQLWebSocketRequest request2) => !(request1 == request2); + + } + +} diff --git a/src/GraphQL.Client/Websocket/GraphQLWebSocketResponse.cs b/src/GraphQL.Client/Websocket/GraphQLWebSocketResponse.cs new file mode 100644 index 00000000..08171a6a --- /dev/null +++ b/src/GraphQL.Client/Websocket/GraphQLWebSocketResponse.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; + +namespace GraphQL.Client.Http.Websocket { + + /// + /// A Subscription Response + /// + public class GraphQLWebSocketResponse : IEquatable { + + /// + /// The Identifier of the Response + /// + public string Id { get; set; } + + /// + /// The Type of the Response + /// + public string Type { get; set; } + + /// + public override bool Equals(object obj) => this.Equals(obj as GraphQLWebSocketResponse); + + /// + public bool Equals(GraphQLWebSocketResponse 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; + } + + 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); + return hashCode; + } + + /// + public static bool operator ==(GraphQLWebSocketResponse response1, GraphQLWebSocketResponse response2) => + EqualityComparer.Default.Equals(response1, response2); + + /// + public static bool operator !=(GraphQLWebSocketResponse response1, GraphQLWebSocketResponse response2) => + !(response1 == response2); + + } + + public class GraphQLWebSocketResponse : GraphQLWebSocketResponse, IEquatable> { + public GraphQLHttpResponse Payload { get; set; } + + public bool Equals(GraphQLWebSocketResponse? other) { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return base.Equals(other) && Payload.Equals(other.Payload); + } + + 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((GraphQLWebSocketResponse)obj); + } + + public override int GetHashCode() { + unchecked { + return (base.GetHashCode() * 397) ^ Payload.GetHashCode(); + } + } + } +} diff --git a/src/GraphQL.Client/Websocket/WebsocketResponseWrapper.cs b/src/GraphQL.Client/Websocket/WebsocketResponseWrapper.cs new file mode 100644 index 00000000..fd91ef78 --- /dev/null +++ b/src/GraphQL.Client/Websocket/WebsocketResponseWrapper.cs @@ -0,0 +1,9 @@ +using System.Runtime.Serialization; + +namespace GraphQL.Client.Http.Websocket { + public class WebsocketResponseWrapper : GraphQLWebSocketResponse { + + [IgnoreDataMember] + public byte[] MessageBytes { get; set; } + } +} diff --git a/src/GraphQL.Primitives/GraphQL.Primitives.csproj b/src/GraphQL.Primitives/GraphQL.Primitives.csproj index ca03991d..53f58f4b 100644 --- a/src/GraphQL.Primitives/GraphQL.Primitives.csproj +++ b/src/GraphQL.Primitives/GraphQL.Primitives.csproj @@ -5,11 +5,6 @@ GraphQL basic types GraphQL - netstandard1.0;netstandard2.0 + netstandard2.0 - - - - - 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.Default.Equals(Variables, other.Variables); } /// @@ -100,7 +68,10 @@ public bool Equals(GraphQLRequest? other) { /// public override int GetHashCode() { unchecked { - return base.GetHashCode() * 397 ^ EqualityComparer.Default.GetHashCode(this.Variables); + var hashCode = Query.GetHashCode(); + hashCode = (hashCode * 397) ^ OperationName?.GetHashCode() ?? 0; + hashCode = (hashCode * 397) ^ Variables?.GetHashCode() ?? 0; + return hashCode; } } @@ -110,7 +81,7 @@ public override int GetHashCode() { /// 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); + public static bool operator ==(GraphQLRequest? left, GraphQLRequest? right) => EqualityComparer.Default.Equals(left, right); /// /// Tests whether two specified instances are not equal @@ -118,8 +89,8 @@ public override int GetHashCode() { /// 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 static bool operator !=(GraphQLRequest? left, GraphQLRequest? right) => !(left == right); } + } diff --git a/src/GraphQL.Client/GraphQLResponse.cs b/src/GraphQL.Primitives/GraphQLResponse.cs similarity index 74% rename from src/GraphQL.Client/GraphQLResponse.cs rename to src/GraphQL.Primitives/GraphQLResponse.cs index 1f203156..4ae09061 100644 --- a/src/GraphQL.Client/GraphQLResponse.cs +++ b/src/GraphQL.Primitives/GraphQLResponse.cs @@ -1,23 +1,27 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.Serialization; -namespace GraphQL.Client { +namespace GraphQL { public class GraphQLResponse : IEquatable?> { + [DataMember(Name = "data")] public T Data { get; set; } + [DataMember(Name = "errors")] public GraphQLError[]? Errors { get; set; } - public IDictionary? Extensions { get; set; } + [DataMember(Name = "extensions")] + public IDictionary? Extensions { get; set; } public override bool Equals(object? obj) => this.Equals(obj as GraphQLResponse); public bool Equals(GraphQLResponse? other) { if (other == null) { return false; } if (ReferenceEquals(this, other)) { return true; } - if (!EqualityComparer.Default.Equals(this.Data, other.Data)) { return false; } + if (!EqualityComparer.Default.Equals(this.Data, other.Data)) { return false; } { if (this.Errors != null && other.Errors != null) { if (!Enumerable.SequenceEqual(this.Errors, other.Errors)) { return false; } @@ -25,13 +29,13 @@ public bool Equals(GraphQLResponse? other) { else if (this.Errors != null && other.Errors == null) { return false; } else if (this.Errors == null && other.Errors != null) { return false; } } - if (!EqualityComparer?>.Default.Equals(this.Extensions, other.Extensions)) { return false; } + if (!EqualityComparer?>.Default.Equals(this.Extensions, other.Extensions)) { return false; } return true; } public override int GetHashCode() { unchecked { - var hashCode = EqualityComparer.Default.GetHashCode(this.Data); + var hashCode = EqualityComparer.Default.GetHashCode(this.Data); { if (this.Errors != null) { foreach (var element in this.Errors) { @@ -42,7 +46,7 @@ public override int GetHashCode() { hashCode = (hashCode * 397) ^ 0; } } - hashCode = (hashCode * 397) ^ EqualityComparer?>.Default.GetHashCode(this.Extensions); + hashCode = (hashCode * 397) ^ EqualityComparer?>.Default.GetHashCode(this.Extensions); return hashCode; } } @@ -54,9 +58,6 @@ public override int GetHashCode() { } - /// - /// The dynamic version of - /// - public class GraphQLResponse : GraphQLResponse { } + } diff --git a/src/src.props b/src/src.props index d9a5800d..19e1fafb 100644 --- a/src/src.props +++ b/src/src.props @@ -7,4 +7,30 @@ true + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + false + false + false + false + + $(GitVersion_FullSemVer) + $(GitVersion_MajorMinorPatch) + $(GitVersion_NuGetPreReleaseTag) + $(GitVersion_PreReleaseTag) + $(GitVersion_NuGetVersion) + $(GitVersion_FullSemVer) + $(GitVersion_InformationalVersion) + $(GitVersion_AssemblySemVer) + $(GitVersion_AssemblySemFileVer) + $(GitVersion_BranchName) + $(GitVersion_Sha) + + 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 { + public IEnumerator GetEnumerator() { + yield return new object[] { 1, "Luke" }; + yield return new object[] { 2, "Vader" }; + } + + IEnumerator IEnumerable.GetEnumerator() { + return GetEnumerator(); + } + } +} diff --git a/tests/GraphQL.Integration.Tests/WebsocketTest.cs b/tests/GraphQL.Integration.Tests/WebsocketTest.cs new file mode 100644 index 00000000..4bd1fade --- /dev/null +++ b/tests/GraphQL.Integration.Tests/WebsocketTest.cs @@ -0,0 +1,331 @@ +using System; +using System.Diagnostics; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using GraphQL.Client.Abstractions; +using GraphQL.Integration.Tests.Extensions; +using GraphQL.Integration.Tests.Helpers; +using IntegrationTestServer; +using Microsoft.AspNetCore.Hosting; +using Xunit; +using Xunit.Abstractions; + +namespace GraphQL.Integration.Tests { + public class WebsocketTest { + private readonly ITestOutputHelper output; + + private static IWebHost CreateServer(int port) => WebHostHelpers.CreateServer(port); + + public WebsocketTest(ITestOutputHelper output) { + this.output = output; + } + + [Fact] + public async void AssertTestingHarness() { + var port = NetworkHelpers.GetFreeTcpPortNumber(); + using (CreateServer(port)) { + var client = WebHostHelpers.GetGraphQLClient(port); + + const string message = "some random testing message"; + var response = await client.AddMessageAsync(message).ConfigureAwait(false); + + Assert.Equal(message, response.Data.AddMessage.Content); + } + } + + [Fact] + public async void CanSendRequestViaWebsocket() { + var port = NetworkHelpers.GetFreeTcpPortNumber(); + using (CreateServer(port)) { + var client = WebHostHelpers.GetGraphQLClient(port, true); + const string message = "some random testing message"; + var response = await client.AddMessageAsync(message).ConfigureAwait(false); + + Assert.Equal(message, response.Data.AddMessage.Content); + } + } + + [Fact] + public async void CanHandleRequestErrorViaWebsocket() { + var port = NetworkHelpers.GetFreeTcpPortNumber(); + using (CreateServer(port)) { + var client = WebHostHelpers.GetGraphQLClient(port, true); + var response = await client.SendQueryAsync("this query is formatted quite badly").ConfigureAwait(false); + + Assert.Single(response.Errors); + } + } + + private const string SubscriptionQuery = @" + subscription { + messageAdded{ + content + } + }"; + + private readonly GraphQLRequest SubscriptionRequest = new GraphQLRequest(SubscriptionQuery); + + [Fact] + public async void CanCreateObservableSubscription() { + var port = NetworkHelpers.GetFreeTcpPortNumber(); + using (CreateServer(port)) { + var client = WebHostHelpers.GetGraphQLClient(port); + await client.InitializeWebsocketConnection(); + + Debug.WriteLine("creating subscription stream"); + IObservable> observable = client.CreateSubscriptionStream(SubscriptionRequest); + + Debug.WriteLine("subscribing..."); + var tester = observable.SubscribeTester(); + const string message1 = "Hello World"; + + var response = await client.AddMessageAsync(message1).ConfigureAwait(false); + Assert.Equal(message1, response.Data.AddMessage.Content); + + tester.ShouldHaveReceivedUpdate(gqlResponse => { + Assert.Equal(message1, gqlResponse.Data.MessageAdded.Content); + }); + + const string message2 = "lorem ipsum dolor si amet"; + response = await client.AddMessageAsync(message2).ConfigureAwait(false); + Assert.Equal(message2, response.Data.AddMessage.Content); + tester.ShouldHaveReceivedUpdate(gqlResponse => { + Assert.Equal(message2, gqlResponse.Data.MessageAdded.Content); + }); + + // disposing the client should throw a TaskCanceledException on the subscription + client.Dispose(); + tester.ShouldHaveCompleted(); + } + } + + public class MessageAddedSubscriptionResult { + public MessageAddedContent MessageAdded { get; set; } + + public class MessageAddedContent { + public string Content { get; set; } + } + } + + [Fact] + public async void CanReconnectWithSameObservable() { + var port = NetworkHelpers.GetFreeTcpPortNumber(); + using (CreateServer(port)) { + var client = WebHostHelpers.GetGraphQLClient(port); + await client.InitializeWebsocketConnection(); + + Debug.WriteLine("creating subscription stream"); + IObservable> observable = client.CreateSubscriptionStream(SubscriptionRequest); + + Debug.WriteLine("subscribing..."); + var tester = observable.SubscribeTester(); + + const string message1 = "Hello World"; + var response = await client.AddMessageAsync(message1).ConfigureAwait(false); + Assert.Equal(message1, response.Data.AddMessage.Content); + tester.ShouldHaveReceivedUpdate(gqlResponse => { + Assert.Equal(message1, gqlResponse.Data.MessageAdded.Content); + }); + + const string message2 = "How are you?"; + response = await client.AddMessageAsync(message2).ConfigureAwait(false); + Assert.Equal(message2, response.Data.AddMessage.Content); + tester.ShouldHaveReceivedUpdate(gqlResponse => { + Assert.Equal(message2, gqlResponse.Data.MessageAdded.Content); + }); + + Debug.WriteLine("disposing subscription..."); + tester.Dispose(); + await Task.Delay(500); + await client.InitializeWebsocketConnection(); + + Debug.WriteLine("creating new subscription..."); + tester = observable.SubscribeTester(); + tester.ShouldHaveReceivedUpdate( + gqlResponse => { Assert.Equal(message2, gqlResponse.Data.MessageAdded.Content); }, + TimeSpan.FromSeconds(10)); + const string message3 = "lorem ipsum dolor si amet"; + response = await client.AddMessageAsync(message3).ConfigureAwait(false); + Assert.Equal(message3, response.Data.AddMessage.Content); + tester.ShouldHaveReceivedUpdate(gqlResponse => { + Assert.Equal(message3, gqlResponse.Data.MessageAdded.Content); + }); + + // disposing the client should complete the subscription + client.Dispose(); + tester.ShouldHaveCompleted(); + } + } + + private const string SubscriptionQuery2 = @" + subscription { + userJoined{ + displayName + id + } + }"; + + public class UserJoinedSubscriptionResult { + public UserJoinedContent UserJoined { get; set; } + + public class UserJoinedContent { + public string DisplayName { get; set; } + public string Id { get; set; } + } + + } + + private readonly GraphQLRequest SubscriptionRequest2 = new GraphQLRequest(SubscriptionQuery2); + + [Fact] + public async void CanConnectTwoSubscriptionsSimultaneously() { + var port = NetworkHelpers.GetFreeTcpPortNumber(); + var callbackTester = new CallbackTester(); + var callbackTester2 = new CallbackTester(); + using (CreateServer(port)) { + var client = WebHostHelpers.GetGraphQLClient(port); + await client.InitializeWebsocketConnection(); + + Debug.WriteLine("creating subscription stream"); + IObservable> observable1 = client.CreateSubscriptionStream(SubscriptionRequest, callbackTester.Callback); + IObservable> observable2 = client.CreateSubscriptionStream(SubscriptionRequest2, callbackTester2.Callback); + + Debug.WriteLine("subscribing..."); + var tester = observable1.SubscribeTester(); + var tester2 = observable2.SubscribeTester(); + + const string message1 = "Hello World"; + var response = await client.AddMessageAsync(message1).ConfigureAwait(false); + Assert.Equal(message1, response.Data.AddMessage.Content); + tester.ShouldHaveReceivedUpdate(gqlResponse => { + Assert.Equal(message1, gqlResponse.Data.MessageAdded.Content); + }); + + await Task.Delay(500); // ToDo: can be removed after https://github.com/graphql-dotnet/server/pull/199 was merged and released + + var joinResponse = await client.JoinDeveloperUser().ConfigureAwait(false); + Assert.Equal("developer", joinResponse.Data.Join.DisplayName); + + tester2.ShouldHaveReceivedUpdate(gqlResponse => { + Assert.Equal("1", gqlResponse.Data.UserJoined.Id); + Assert.Equal("developer", gqlResponse.Data.UserJoined.DisplayName); + }); + + Debug.WriteLine("disposing subscription..."); + tester2.Dispose(); + + const string message3 = "lorem ipsum dolor si amet"; + response = await client.AddMessageAsync(message3).ConfigureAwait(false); + Assert.Equal(message3, response.Data.AddMessage.Content); + tester.ShouldHaveReceivedUpdate(gqlResponse => { + Assert.Equal(message3, gqlResponse.Data.MessageAdded.Content); + }); + + // disposing the client should complete the subscription + client.Dispose(); + tester.ShouldHaveCompleted(); + } + } + + [Fact] + public async void CanHandleConnectionTimeout() { + var port = NetworkHelpers.GetFreeTcpPortNumber(); + var server = CreateServer(port); + var callbackTester = new CallbackTester(); + + var client = WebHostHelpers.GetGraphQLClient(port); + await client.InitializeWebsocketConnection(); + Debug.WriteLine("creating subscription stream"); + IObservable> observable = client.CreateSubscriptionStream(SubscriptionRequest, callbackTester.Callback); + + Debug.WriteLine("subscribing..."); + var tester = observable.SubscribeTester(); + const string message1 = "Hello World"; + + var response = await client.AddMessageAsync(message1).ConfigureAwait(false); + Assert.Equal(message1, response.Data.AddMessage.Content); + + tester.ShouldHaveReceivedUpdate(gqlResponse => { + Assert.Equal(message1, gqlResponse.Data.MessageAdded.Content); + }); + + Debug.WriteLine("stopping web host..."); + await server.StopAsync(CancellationToken.None).ConfigureAwait(false); + Debug.WriteLine("web host stopped..."); + + callbackTester.CallbackShouldHaveBeenInvoked(exception => { + Assert.IsType(exception); + }, TimeSpan.FromSeconds(10)); + + try { + server.Start(); + } + catch (Exception e) { + output.WriteLine($"failed to restart server: {e}"); + } + + // disposing the client should complete the subscription + client.Dispose(); + tester.ShouldHaveCompleted(TimeSpan.FromSeconds(5)); + server.Dispose(); + } + + [Fact] + public async void CanHandleSubscriptionError() { + var port = NetworkHelpers.GetFreeTcpPortNumber(); + using (CreateServer(port)) { + var client = WebHostHelpers.GetGraphQLClient(port); + await client.InitializeWebsocketConnection(); + Debug.WriteLine("creating subscription stream"); + IObservable> observable = client.CreateSubscriptionStream( + new GraphQLRequest(@" + subscription { + failImmediately { + content + } + }") + ); + + Debug.WriteLine("subscribing..."); + var tester = observable.SubscribeTester(); + tester.ShouldHaveReceivedUpdate(gqlResponse => { + Assert.Single(gqlResponse.Errors); + }); + tester.ShouldHaveCompleted(); + + client.Dispose(); + } + } + + [Fact] + public async void CanHandleQueryErrorInSubscription() { + var port = NetworkHelpers.GetFreeTcpPortNumber(); + using (CreateServer(port)) { + + var test = new GraphQLRequest("tset", new { test = "blaa" }); + + var client = WebHostHelpers.GetGraphQLClient(port); + await client.InitializeWebsocketConnection(); + Debug.WriteLine("creating subscription stream"); + IObservable> observable = client.CreateSubscriptionStream( + new GraphQLRequest(@" + subscription { + fieldDoesNotExist { + content + } + }") + ); + + Debug.WriteLine("subscribing..."); + var tester = observable.SubscribeTester(); + tester.ShouldHaveReceivedUpdate(gqlResponse => { + Assert.Single(gqlResponse.Errors); + }); + tester.ShouldHaveCompleted(); + + client.Dispose(); + } + } + } +} diff --git a/tests/GraphQL.Primitives.Tests/GraphQL.Primitives.Tests.csproj b/tests/GraphQL.Primitives.Tests/GraphQL.Primitives.Tests.csproj index 99070b4c..c7be7594 100644 --- a/tests/GraphQL.Primitives.Tests/GraphQL.Primitives.Tests.csproj +++ b/tests/GraphQL.Primitives.Tests/GraphQL.Primitives.Tests.csproj @@ -6,8 +6,16 @@ netcoreapp3.1 + + + + + + + + diff --git a/tests/GraphQL.Client.Tests/GraphQLLocationTest.cs b/tests/GraphQL.Primitives.Tests/GraphQLLocationTest.cs similarity index 97% rename from tests/GraphQL.Client.Tests/GraphQLLocationTest.cs rename to tests/GraphQL.Primitives.Tests/GraphQLLocationTest.cs index 468d521e..5b144a74 100644 --- a/tests/GraphQL.Client.Tests/GraphQLLocationTest.cs +++ b/tests/GraphQL.Primitives.Tests/GraphQLLocationTest.cs @@ -1,6 +1,6 @@ using Xunit; -namespace GraphQL.Client.Tests { +namespace GraphQL.Primitives.Tests { public class GraphQLLocationTest { diff --git a/tests/GraphQL.Primitives.Tests/GraphQLLocationTests.cs b/tests/GraphQL.Primitives.Tests/GraphQLLocationTests.cs deleted file mode 100644 index 3b13b068..00000000 --- a/tests/GraphQL.Primitives.Tests/GraphQLLocationTests.cs +++ /dev/null @@ -1,204 +0,0 @@ -using Xunit; - -namespace GraphQL.Primitives.Tests { - - public class GraphQLLocationTests { - - [Fact] - public void Constructor1() { - var graphQLLocation = new GraphQLLocation(); - Assert.NotNull(graphQLLocation); - } - - [Fact] - public void Constructor2() { - var graphQLLocation = new GraphQLLocation { - Column = 10 - }; - Assert.Equal(10U, graphQLLocation.Column); - } - - [Fact] - public void Constructor3() { - var graphQLLocation = new GraphQLLocation { - Line = 10 - }; - Assert.Equal(10U, graphQLLocation.Line); - } - - [Fact] - public void Constructor4() { - var graphQLLocation = new GraphQLLocation { - Column = 10, - Line = 10 - }; - Assert.Equal(10U, graphQLLocation.Column); - Assert.Equal(10U, graphQLLocation.Line); - } - - [Fact] - public void Equality1() { - var graphQLLocation1 = new GraphQLLocation(); - var graphQLLocation2 = new GraphQLLocation(); - Assert.True(graphQLLocation1.Equals(graphQLLocation2)); - Assert.True(graphQLLocation2.Equals(graphQLLocation1)); - } - - [Fact] - public void Equality2() { - var graphQLLocation1 = new GraphQLLocation { - Column = 1, - Line = 2 - }; - var graphQLLocation2 = new GraphQLLocation { - Column = 1, - Line = 2 - }; - Assert.True(graphQLLocation1.Equals(graphQLLocation2)); - Assert.True(graphQLLocation2.Equals(graphQLLocation1)); - } - - [Fact] - public void Equality3() { - var graphQLLocation1 = new GraphQLLocation(); - var graphQLLocation2 = new GraphQLLocation(); - Assert.True(graphQLLocation1 == graphQLLocation2); - Assert.True(graphQLLocation2 == graphQLLocation1); - } - - [Fact] - public void Equality4() { - var graphQLLocation1 = new GraphQLLocation { - Column = 1, - Line = 2 - }; - var graphQLLocation2 = new GraphQLLocation { - Column = 1, - Line = 2 - }; - Assert.True(graphQLLocation1 == graphQLLocation2); - Assert.True(graphQLLocation2 == graphQLLocation1); - } - - [Fact] - public void Equality5() { - var graphQLLocation1 = new GraphQLLocation(); - var graphQLLocation2 = new GraphQLLocation(); - Assert.True(graphQLLocation1.Equals((object)graphQLLocation2)); - Assert.True(graphQLLocation2.Equals((object)graphQLLocation1)); - } - - [Fact] - public void Equality6() { - var graphQLLocation1 = new GraphQLLocation { - Column = 1, - Line = 2 - }; - var graphQLLocation2 = new GraphQLLocation { - Column = 1, - Line = 2 - }; - Assert.True(graphQLLocation1.Equals((object)graphQLLocation2)); - Assert.True(graphQLLocation2.Equals((object)graphQLLocation1)); - } - - [Fact] - public void GetHashCode1() { - var graphQLLocation = new GraphQLLocation(); - Assert.Equal(0, graphQLLocation.GetHashCode()); - } - - [Fact] - public void GetHashCode2() { - var graphQLLocation = new GraphQLLocation { - Column = 1 - }; - Assert.Equal(1.GetHashCode(), graphQLLocation.GetHashCode()); - } - - [Fact] - public void GetHashCode3() { - var graphQLLocation = new GraphQLLocation { - Line = 1 - }; - Assert.Equal(0.GetHashCode() ^ 1.GetHashCode(), graphQLLocation.GetHashCode()); - } - - [Fact] - public void GetHashCode4() { - var graphQLLocation = new GraphQLLocation { - Column = 1, - Line = 2 - }; - Assert.Equal(1.GetHashCode() ^ 2.GetHashCode(), graphQLLocation.GetHashCode()); - } - - [Fact] - public void Inequality1() { - var graphQLLocation1 = new GraphQLLocation(); - var graphQLLocation2 = new GraphQLLocation { Column = 1, Line = 2 }; - Assert.False(graphQLLocation1.Equals(graphQLLocation2)); - Assert.False(graphQLLocation2.Equals(graphQLLocation1)); - } - - [Fact] - public void Inequality2() { - var graphQLLocation1 = new GraphQLLocation { - Column = 1, - Line = 2 - }; - var graphQLLocation2 = new GraphQLLocation { - Column = 2, - Line = 1 - }; - Assert.False(graphQLLocation1.Equals(graphQLLocation2)); - Assert.False(graphQLLocation2.Equals(graphQLLocation1)); - } - - [Fact] - public void Inequality3() { - var graphQLLocation1 = new GraphQLLocation(); - var graphQLLocation2 = new GraphQLLocation { Column = 1, Line = 2 }; - Assert.True(graphQLLocation1 != graphQLLocation2); - Assert.True(graphQLLocation2 != graphQLLocation1); - } - - [Fact] - public void Inequality4() { - var graphQLLocation1 = new GraphQLLocation { - Column = 1, - Line = 2 - }; - var graphQLLocation2 = new GraphQLLocation { - Column = 2, - Line = 1 - }; - Assert.True(graphQLLocation1 != graphQLLocation2); - Assert.True(graphQLLocation2 != graphQLLocation1); - } - - [Fact] - public void Inequality5() { - var graphQLLocation1 = new GraphQLLocation(); - var graphQLLocation2 = new GraphQLLocation { Column = 1, Line = 2 }; - Assert.False(graphQLLocation1.Equals((object)graphQLLocation2)); - Assert.False(graphQLLocation2.Equals((object)graphQLLocation1)); - } - - [Fact] - public void Inequality6() { - var graphQLLocation1 = new GraphQLLocation { - Column = 1, - Line = 2 - }; - var graphQLLocation2 = new GraphQLLocation { - Column = 2, - Line = 1 - }; - Assert.False(graphQLLocation1.Equals((object)graphQLLocation2)); - Assert.False(graphQLLocation2.Equals((object)graphQLLocation1)); - } - - } - -} diff --git a/tests/GraphQL.Primitives.Tests/GraphQLRequestTest.cs b/tests/GraphQL.Primitives.Tests/GraphQLRequestTest.cs new file mode 100644 index 00000000..f4a22742 --- /dev/null +++ b/tests/GraphQL.Primitives.Tests/GraphQLRequestTest.cs @@ -0,0 +1,157 @@ +using Xunit; + +namespace GraphQL.Primitives.Tests { + + public class GraphQLRequestTest { + + [Fact] + public void ConstructorFact() { + var graphQLRequest = new GraphQLRequest("{hero{name}}"); + Assert.NotNull(graphQLRequest.Query); + Assert.Null(graphQLRequest.OperationName); + Assert.Null(graphQLRequest.Variables); + } + + [Fact] + public void ConstructorExtendedFact() { + var graphQLRequest = new GraphQLRequest("{hero{name}}", new { varName = "varValue" }, "operationName"); + Assert.NotNull(graphQLRequest.Query); + Assert.NotNull(graphQLRequest.OperationName); + Assert.NotNull(graphQLRequest.Variables); + } + + [Fact] + public void Equality1Fact() { + var graphQLRequest = new GraphQLRequest("{hero{name}}"); + Assert.Equal(graphQLRequest, graphQLRequest); + } + + [Fact] + public void Equality2Fact() { + var graphQLRequest1 = new GraphQLRequest("{hero{name}}"); + var graphQLRequest2 = new GraphQLRequest("{hero{name}}"); + Assert.Equal(graphQLRequest1, graphQLRequest2); + } + + [Fact] + public void Equality3Fact() { + var graphQLRequest1 = new GraphQLRequest("{hero{name}}", new { varName = "varValue" }, "operationName"); + var graphQLRequest2 = new GraphQLRequest("{hero{name}}", new { varName = "varValue" }, "operationName"); + Assert.Equal(graphQLRequest1, graphQLRequest2); + } + + + [Fact] + public void Equality4Fact() { + var graphQLRequest1 = new GraphQLRequest("{hero{name}}", new { varName = "varValue1" }, "operationName"); + var graphQLRequest2 = new GraphQLRequest("{hero{name}}", new { varName = "varValue2" }, "operationName"); + Assert.NotEqual(graphQLRequest1, graphQLRequest2); + } + + [Fact] + public void EqualityOperatorFact() { + var graphQLRequest1 = new GraphQLRequest("{hero{name}}", new { varName = "varValue" }, "operationName"); + var graphQLRequest2 = new GraphQLRequest("{hero{name}}", new { varName = "varValue" }, "operationName"); + Assert.True(graphQLRequest1 == graphQLRequest2); + } + + [Fact] + public void InEquality1Fact() { + var graphQLRequest1 = new GraphQLRequest("{hero{name1}}"); + var graphQLRequest2 = new GraphQLRequest("{hero{name2}}"); + Assert.NotEqual(graphQLRequest1, graphQLRequest2); + } + + [Fact] + public void InEquality2Fact() { + GraphQLRequest graphQLRequest1 = new GraphQLRequest("{hero{name}}", new { varName = "varValue1" }, "operationName"); + GraphQLRequest graphQLRequest2 = new GraphQLRequest("{hero{name}}", new { varName = "varValue2" }, "operationName"); + Assert.NotEqual(graphQLRequest1, graphQLRequest2); + } + + [Fact] + public void InEqualityOperatorFact() { + var graphQLRequest1 = new GraphQLRequest("{hero{name1}}"); + var graphQLRequest2 = new GraphQLRequest("{hero{name2}}"); + Assert.True(graphQLRequest1 != graphQLRequest2); + } + + [Fact] + public void GetHashCode1Fact() { + var graphQLRequest1 = new GraphQLRequest("{hero{name}}"); + var graphQLRequest2 = new GraphQLRequest("{hero{name}}"); + Assert.True(graphQLRequest1.GetHashCode() == graphQLRequest2.GetHashCode()); + } + + [Fact] + public void GetHashCode2Fact() { + var graphQLRequest1 = new GraphQLRequest("{hero{name}}", new { varName = "varValue" }, "operationName"); + var graphQLRequest2 = new GraphQLRequest("{hero{name}}", new { varName = "varValue" }, "operationName"); + Assert.True(graphQLRequest1.GetHashCode() == graphQLRequest2.GetHashCode()); + } + + [Fact] + public void GetHashCode3Fact() { + var graphQLRequest1 = new GraphQLRequest("{hero{name}}", new { varName = "varValue1" }, "operationName"); + var graphQLRequest2 = new GraphQLRequest("{hero{name}}", new { varName = "varValue2" }, "operationName"); + Assert.True(graphQLRequest1.GetHashCode() != graphQLRequest2.GetHashCode()); + } + + [Fact] + public void PropertyQueryGetFact() { + var graphQLRequest = new GraphQLRequest("{hero{name}}", new { varName = "varValue1" }, "operationName"); + Assert.Equal("{hero{name}}", graphQLRequest.Query); + } + + [Fact] + public void PropertyQuerySetFact() { + var graphQLRequest = new GraphQLRequest("{hero{name}}", new { varName = "varValue1" }, "operationName"); + graphQLRequest.Query = "{hero{name2}}"; + Assert.Equal("{hero{name2}}", graphQLRequest.Query); + } + + [Fact] + public void PropertyOperationNameGetFact() { + var graphQLRequest = new GraphQLRequest("{hero{name}}", new { varName = "varValue" }, "operationName"); + Assert.Equal("operationName", graphQLRequest.OperationName); + } + + [Fact] + public void PropertyOperationNameNullGetFact() { + var graphQLRequest = new GraphQLRequest("{hero{name}}"); + Assert.Null(graphQLRequest.OperationName); + } + + [Fact] + public void PropertyOperationNameSetFact() { + var graphQLRequest = new GraphQLRequest("{hero{name}}", new { varName = "varValue" }, "operationName1"); + graphQLRequest.OperationName = "operationName2"; + Assert.Equal("operationName2", graphQLRequest.OperationName); + } + + [Fact] + public void PropertyVariableGetFact() { + var graphQLRequest = new GraphQLRequest("{hero{name}}", new { varName = "varValue" }, "operationName"); + Assert.Equal(new { varName = "varValue" }, graphQLRequest.Variables); + } + + [Fact] + public void PropertyVariableNullGetFact() { + var graphQLRequest = new GraphQLRequest("{hero{name}}"); + Assert.Null(graphQLRequest.Variables); + } + + [Fact] + public void PropertyVariableSetFact() { + var graphQLRequest = new GraphQLRequest("{hero{name}}", new { varName = "varValue1" }, "operationName1"); + graphQLRequest.Variables = new { + varName = "varValue2" + }; + Assert.Equal(new { + varName = "varValue2" + }, graphQLRequest.Variables); + } + + } + +} diff --git a/tests/GraphQL.Client.Tests/GraphQLResponseTest.cs b/tests/GraphQL.Primitives.Tests/GraphQLResponseTest.cs similarity index 68% rename from tests/GraphQL.Client.Tests/GraphQLResponseTest.cs rename to tests/GraphQL.Primitives.Tests/GraphQLResponseTest.cs index e124d134..07c4063c 100644 --- a/tests/GraphQL.Client.Tests/GraphQLResponseTest.cs +++ b/tests/GraphQL.Primitives.Tests/GraphQLResponseTest.cs @@ -1,19 +1,19 @@ using Xunit; -namespace GraphQL.Client.Tests { +namespace GraphQL.Primitives.Tests { public class GraphQLResponseTest { [Fact] public void Constructor1Fact() { - var graphQLResponse = new GraphQLResponse(); + var graphQLResponse = new GraphQLResponse(); Assert.Null(graphQLResponse.Data); Assert.Null(graphQLResponse.Errors); } [Fact] public void Constructor2Fact() { - var graphQLResponse = new GraphQLResponse { + var graphQLResponse = new GraphQLResponse { Data = new { a = 1 }, Errors = new[] { new GraphQLError { Message = "message" } } }; @@ -23,24 +23,24 @@ public void Constructor2Fact() { [Fact] public void Equality1Fact() { - var graphQLResponse = new GraphQLResponse(); + var graphQLResponse = new GraphQLResponse(); Assert.Equal(graphQLResponse, graphQLResponse); } [Fact] public void Equality2Fact() { - var graphQLResponse1 = new GraphQLResponse(); - var graphQLResponse2 = new GraphQLResponse(); + var graphQLResponse1 = new GraphQLResponse(); + var graphQLResponse2 = new GraphQLResponse(); Assert.Equal(graphQLResponse1, graphQLResponse2); } [Fact] public void Equality3Fact() { - var graphQLResponse1 = new GraphQLResponse { + var graphQLResponse1 = new GraphQLResponse { Data = new { a = 1 }, Errors = new[] { new GraphQLError { Message = "message" } } }; - var graphQLResponse2 = new GraphQLResponse { + var graphQLResponse2 = new GraphQLResponse { Data = new { a = 1 }, Errors = new[] { new GraphQLError { Message = "message" } } }; @@ -49,18 +49,18 @@ public void Equality3Fact() { [Fact] public void EqualityOperatorFact() { - var graphQLResponse1 = new GraphQLResponse(); - var graphQLResponse2 = new GraphQLResponse(); + var graphQLResponse1 = new GraphQLResponse(); + var graphQLResponse2 = new GraphQLResponse(); Assert.True(graphQLResponse1 == graphQLResponse2); } [Fact] public void InEqualityFact() { - var graphQLResponse1 = new GraphQLResponse { + var graphQLResponse1 = new GraphQLResponse { Data = new { a = 1 }, Errors = new[] { new GraphQLError { Message = "message" } } }; - var graphQLResponse2 = new GraphQLResponse { + var graphQLResponse2 = new GraphQLResponse { Data = new { a = 2 }, Errors = new[] { new GraphQLError { Message = "message" } } }; @@ -69,11 +69,11 @@ public void InEqualityFact() { [Fact] public void InEqualityOperatorFact() { - var graphQLResponse1 = new GraphQLResponse { + var graphQLResponse1 = new GraphQLResponse { Data = new { a = 1 }, Errors = new[] { new GraphQLError { Message = "message" } } }; - var graphQLResponse2 = new GraphQLResponse { + var graphQLResponse2 = new GraphQLResponse { Data = new { a = 2 }, Errors = new[] { new GraphQLError { Message = "message" } } }; @@ -82,11 +82,11 @@ public void InEqualityOperatorFact() { [Fact] public void GetHashCodeFact() { - var graphQLResponse1 = new GraphQLResponse { + var graphQLResponse1 = new GraphQLResponse { Data = new { a = 1 }, Errors = new[] { new GraphQLError { Message = "message" } } }; - var graphQLResponse2 = new GraphQLResponse { + var graphQLResponse2 = new GraphQLResponse { Data = new { a = 1 }, Errors = new[] { new GraphQLError { Message = "message" } } }; diff --git a/tests/GraphQL.Primitives.Tests/JsonSerializationTests.cs b/tests/GraphQL.Primitives.Tests/JsonSerializationTests.cs new file mode 100644 index 00000000..3dbdb828 --- /dev/null +++ b/tests/GraphQL.Primitives.Tests/JsonSerializationTests.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Text.Json; +using FluentAssertions; +using Xunit; + +namespace GraphQL.Primitives.Tests { + public class JsonSerializationTests { + + [Fact] + public void WebSocketResponseDeserialization() { + var testObject = new ExtendedTestObject { Id = "test", OtherData = "this is some other stuff" }; + var json = JsonSerializer.Serialize(testObject); + var deserialized = JsonSerializer.Deserialize(json); + var dict = JsonSerializer.Deserialize>(json); + var childObject = (JsonElement) dict["ChildObject"]; + childObject.GetProperty("Id").GetString().Should().Be(testObject.ChildObject.Id); + } + + public class TestObject { + public string Id { get; set; } + + } + + public class ExtendedTestObject : TestObject { + public string OtherData { get; set; } + + public TestObject ChildObject{ get; set; } = new TestObject {Id = "1337"}; + } + } +} diff --git a/tests/GraphQL.Server.Test/libman.json b/tests/GraphQL.Server.Test/libman.json new file mode 100644 index 00000000..ceee2710 --- /dev/null +++ b/tests/GraphQL.Server.Test/libman.json @@ -0,0 +1,5 @@ +{ + "version": "1.0", + "defaultProvider": "cdnjs", + "libraries": [] +} \ No newline at end of file diff --git a/tests/IntegrationTestServer/ChatSchema/CapitalizedFieldsGraphType.cs b/tests/IntegrationTestServer/ChatSchema/CapitalizedFieldsGraphType.cs new file mode 100644 index 00000000..32b235e5 --- /dev/null +++ b/tests/IntegrationTestServer/ChatSchema/CapitalizedFieldsGraphType.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Metadata.Ecma335; +using System.Threading.Tasks; +using GraphQL.Types; + +namespace IntegrationTestServer.ChatSchema { + public class CapitalizedFieldsGraphType: ObjectGraphType { + public CapitalizedFieldsGraphType() { + Name = "CapitalizedFields"; + + Field() + .Name("StringField") + .Resolve(context => "hello world"); + } + } +} diff --git a/tests/IntegrationTestServer/ChatSchema/ChatMutation.cs b/tests/IntegrationTestServer/ChatSchema/ChatMutation.cs new file mode 100644 index 00000000..43895aec --- /dev/null +++ b/tests/IntegrationTestServer/ChatSchema/ChatMutation.cs @@ -0,0 +1,35 @@ +using GraphQL.Types; + +namespace IntegrationTestServer.ChatSchema { + public class ChatMutation : ObjectGraphType { + public ChatMutation(IChat chat) { + Field("addMessage", + arguments: new QueryArguments( + new QueryArgument { Name = "message" } + ), + resolve: context => { + var receivedMessage = context.GetArgument("message"); + var message = chat.AddMessage(receivedMessage); + return message; + }); + + Field("join", + arguments: new QueryArguments( + new QueryArgument { Name = "userId" } + ), + resolve: context => { + var userId = context.GetArgument("userId"); + var userJoined = chat.Join(userId); + return userJoined; + }); + } + } + + public class MessageInputType : InputObjectGraphType { + public MessageInputType() { + Field("fromId"); + Field("content"); + Field("sentAt"); + } + } +} diff --git a/tests/IntegrationTestServer/ChatSchema/ChatQuery.cs b/tests/IntegrationTestServer/ChatSchema/ChatQuery.cs new file mode 100644 index 00000000..8b423f95 --- /dev/null +++ b/tests/IntegrationTestServer/ChatSchema/ChatQuery.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Linq; +using GraphQL; +using GraphQL.Types; + +namespace IntegrationTestServer.ChatSchema { + public class ChatQuery : ObjectGraphType { + + public static readonly Dictionary TestExtensions = new Dictionary { + {"extension1", "hello world"}, + {"another extension", 4711} + }; + + public ChatQuery(IChat chat) { + Name = "ChatQuery"; + + Field>("messages", resolve: context => chat.AllMessages.Take(100)); + + Field() + .Name("extensionsTest") + .Resolve(context => { + context.Errors.Add(new ExecutionError("this error contains extension fields", TestExtensions)); + return null; + }); + } + } +} diff --git a/tests/IntegrationTestServer/ChatSchema/ChatSchema.cs b/tests/IntegrationTestServer/ChatSchema/ChatSchema.cs new file mode 100644 index 00000000..3878bcf5 --- /dev/null +++ b/tests/IntegrationTestServer/ChatSchema/ChatSchema.cs @@ -0,0 +1,11 @@ +using GraphQL.Types; + +namespace IntegrationTestServer.ChatSchema { + public class ChatSchema : Schema { + public ChatSchema(IChat chat) { + Query = new ChatQuery(chat); + Mutation = new ChatMutation(chat); + Subscription = new ChatSubscriptions(chat); + } + } +} diff --git a/tests/IntegrationTestServer/ChatSchema/ChatSubscriptions.cs b/tests/IntegrationTestServer/ChatSchema/ChatSubscriptions.cs new file mode 100644 index 00000000..8f3c43ed --- /dev/null +++ b/tests/IntegrationTestServer/ChatSchema/ChatSubscriptions.cs @@ -0,0 +1,88 @@ +using System; +using System.Linq; +using System.Reactive.Linq; +using System.Security.Claims; +using GraphQL; +using GraphQL.Resolvers; +using GraphQL.Server.Transports.Subscriptions.Abstractions; +using GraphQL.Subscription; +using GraphQL.Types; + +namespace IntegrationTestServer.ChatSchema { + public class ChatSubscriptions : ObjectGraphType { + private readonly IChat _chat; + + public ChatSubscriptions(IChat chat) { + _chat = chat; + AddField(new EventStreamFieldType { + Name = "messageAdded", + Type = typeof(MessageType), + Resolver = new FuncFieldResolver(ResolveMessage), + Subscriber = new EventStreamResolver(Subscribe) + }); + + AddField(new EventStreamFieldType { + Name = "contentAdded", + Type = typeof(MessageType), + Resolver = new FuncFieldResolver(ResolveMessage), + Subscriber = new EventStreamResolver(Subscribe) + }); + + AddField(new EventStreamFieldType { + Name = "messageAddedByUser", + Arguments = new QueryArguments( + new QueryArgument> { Name = "id" } + ), + Type = typeof(MessageType), + Resolver = new FuncFieldResolver(ResolveMessage), + Subscriber = new EventStreamResolver(SubscribeById) + }); + + AddField(new EventStreamFieldType { + Name = "userJoined", + Type = typeof(MessageFromType), + Resolver = new FuncFieldResolver(context => context.Source as MessageFrom), + Subscriber = new EventStreamResolver(context => _chat.UserJoined()) + }); + + + AddField(new EventStreamFieldType { + Name = "failImmediately", + Type = typeof(MessageType), + Resolver = new FuncFieldResolver(ResolveMessage), + Subscriber = new EventStreamResolver(context => throw new NotSupportedException("this is supposed to fail")) + }); + } + + private IObservable SubscribeById(ResolveEventStreamContext context) { + var messageContext = context.UserContext.As(); + var user = messageContext.Get("user"); + + var sub = "Anonymous"; + if (user != null) + sub = user.Claims.FirstOrDefault(c => c.Type == "sub")?.Value; + + var messages = _chat.Messages(sub); + + var id = context.GetArgument("id"); + return messages.Where(message => message.From.Id == id); + } + + private Message ResolveMessage(ResolveFieldContext context) { + var message = context.Source as Message; + + return message; + } + + private IObservable Subscribe(ResolveEventStreamContext context) { + var messageContext = context.UserContext.As(); + var user = messageContext.Get("user"); + + var sub = "Anonymous"; + if (user != null) + sub = user.Claims.FirstOrDefault(c => c.Type == "sub")?.Value; + + return _chat.Messages(sub); + } + } +} diff --git a/tests/IntegrationTestServer/ChatSchema/IChat.cs b/tests/IntegrationTestServer/ChatSchema/IChat.cs new file mode 100644 index 00000000..ef1bd08f --- /dev/null +++ b/tests/IntegrationTestServer/ChatSchema/IChat.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Concurrent; +using System.Reactive.Linq; +using System.Reactive.Subjects; + +namespace IntegrationTestServer.ChatSchema { + public interface IChat { + ConcurrentStack AllMessages { get; } + + Message AddMessage(Message message); + + MessageFrom Join(string userId); + + IObservable Messages(string user); + IObservable UserJoined(); + + Message AddMessage(ReceivedMessage message); + } + + public class Chat : IChat { + private readonly ISubject _messageStream = new ReplaySubject(1); + private readonly ISubject _userJoined = new Subject(); + + public Chat() { + AllMessages = new ConcurrentStack(); + Users = new ConcurrentDictionary { + ["1"] = "developer", + ["2"] = "tester" + }; + } + + public ConcurrentDictionary Users { get; set; } + + public ConcurrentStack AllMessages { get; } + + public Message AddMessage(ReceivedMessage message) { + if (!Users.TryGetValue(message.FromId, out var displayName)) { + displayName = "(unknown)"; + } + + return AddMessage(new Message { + Content = message.Content, + SentAt = message.SentAt, + From = new MessageFrom { + DisplayName = displayName, + Id = message.FromId + } + }); + } + + public Message AddMessage(Message message) { + AllMessages.Push(message); + _messageStream.OnNext(message); + return message; + } + + public MessageFrom Join(string userId) { + if (!Users.TryGetValue(userId, out var displayName)) { + displayName = "(unknown)"; + } + + var joinedUser = new MessageFrom { + Id = userId, + DisplayName = displayName + }; + + _userJoined.OnNext(joinedUser); + return joinedUser; + } + + public IObservable Messages(string user) { + return _messageStream + .Select(message => { + message.Sub = user; + return message; + }) + .AsObservable(); + } + + public void AddError(Exception exception) { + _messageStream.OnError(exception); + } + + public IObservable UserJoined() { + return _userJoined.AsObservable(); + } + } + + public class User { + public string Id { get; set; } + public string Name { get; set; } + } +} diff --git a/tests/IntegrationTestServer/ChatSchema/Message.cs b/tests/IntegrationTestServer/ChatSchema/Message.cs new file mode 100644 index 00000000..93451aaa --- /dev/null +++ b/tests/IntegrationTestServer/ChatSchema/Message.cs @@ -0,0 +1,13 @@ +using System; + +namespace IntegrationTestServer.ChatSchema { + public class Message { + public MessageFrom From { get; set; } + + public string Sub { get; set; } + + public string Content { get; set; } + + public DateTime SentAt { get; set; } + } +} diff --git a/tests/IntegrationTestServer/ChatSchema/MessageFrom.cs b/tests/IntegrationTestServer/ChatSchema/MessageFrom.cs new file mode 100644 index 00000000..8dff8852 --- /dev/null +++ b/tests/IntegrationTestServer/ChatSchema/MessageFrom.cs @@ -0,0 +1,7 @@ +namespace IntegrationTestServer.ChatSchema { + public class MessageFrom { + public string Id { get; set; } + + public string DisplayName { get; set; } + } +} diff --git a/tests/IntegrationTestServer/ChatSchema/MessageFromType.cs b/tests/IntegrationTestServer/ChatSchema/MessageFromType.cs new file mode 100644 index 00000000..da243ee9 --- /dev/null +++ b/tests/IntegrationTestServer/ChatSchema/MessageFromType.cs @@ -0,0 +1,10 @@ +using GraphQL.Types; + +namespace IntegrationTestServer.ChatSchema { + public class MessageFromType : ObjectGraphType { + public MessageFromType() { + Field(o => o.Id); + Field(o => o.DisplayName); + } + } +} diff --git a/tests/IntegrationTestServer/ChatSchema/MessageType.cs b/tests/IntegrationTestServer/ChatSchema/MessageType.cs new file mode 100644 index 00000000..6740189a --- /dev/null +++ b/tests/IntegrationTestServer/ChatSchema/MessageType.cs @@ -0,0 +1,17 @@ +using GraphQL.Types; + +namespace IntegrationTestServer.ChatSchema { + public class MessageType : ObjectGraphType { + public MessageType() { + Field(o => o.Content); + Field(o => o.SentAt); + Field(o => o.Sub); + Field(o => o.From, false, typeof(MessageFromType)).Resolve(ResolveFrom); + } + + private MessageFrom ResolveFrom(ResolveFieldContext context) { + var message = context.Source; + return message.From; + } + } +} diff --git a/tests/IntegrationTestServer/ChatSchema/ReceivedMessage.cs b/tests/IntegrationTestServer/ChatSchema/ReceivedMessage.cs new file mode 100644 index 00000000..a3d9025b --- /dev/null +++ b/tests/IntegrationTestServer/ChatSchema/ReceivedMessage.cs @@ -0,0 +1,11 @@ +using System; + +namespace IntegrationTestServer.ChatSchema { + public class ReceivedMessage { + public string FromId { get; set; } + + public string Content { get; set; } + + public DateTime SentAt { get; set; } + } +} diff --git a/tests/IntegrationTestServer/IntegrationTestServer.csproj b/tests/IntegrationTestServer/IntegrationTestServer.csproj new file mode 100644 index 00000000..6ecc6a43 --- /dev/null +++ b/tests/IntegrationTestServer/IntegrationTestServer.csproj @@ -0,0 +1,23 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + + + + + + + diff --git a/tests/IntegrationTestServer/Program.cs b/tests/IntegrationTestServer/Program.cs new file mode 100644 index 00000000..67cbd96b --- /dev/null +++ b/tests/IntegrationTestServer/Program.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Logging; + +namespace IntegrationTestServer { + public class Program { + public static void Main(string[] args) { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup() + .ConfigureLogging((ctx, logging) => logging.SetMinimumLevel(LogLevel.Debug)); + } +} diff --git a/tests/IntegrationTestServer/Properties/launchSettings.json b/tests/IntegrationTestServer/Properties/launchSettings.json new file mode 100644 index 00000000..cd6aedd6 --- /dev/null +++ b/tests/IntegrationTestServer/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:55069/ui/graphiql/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IntegrationTestServer": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "ui/graphiql", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:5005/" + } + } +} \ No newline at end of file diff --git a/tests/IntegrationTestServer/Startup.cs b/tests/IntegrationTestServer/Startup.cs new file mode 100644 index 00000000..0ae7280e --- /dev/null +++ b/tests/IntegrationTestServer/Startup.cs @@ -0,0 +1,70 @@ +using GraphQL; +using GraphQL.Server; +using GraphQL.Server.Ui.GraphiQL; +using GraphQL.Server.Ui.Voyager; +using GraphQL.Server.Ui.Playground; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace IntegrationTestServer { + public abstract class Startup { + protected Startup(IConfiguration configuration, IWebHostEnvironment environment) { + Configuration = configuration; + Environment = environment; + } + + public IConfiguration Configuration { get; } + public IWebHostEnvironment Environment { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) { + services.Configure(options => + { + options.AllowSynchronousIO = true; + }); + + services.AddTransient(provider => new FuncDependencyResolver(provider.GetService)); + + ConfigureGraphQLSchemaServices(services); + + services.AddGraphQL(options => { + options.EnableMetrics = true; + options.ExposeExceptions = Environment.IsDevelopment(); + }) + .AddWebSockets(); + } + + public abstract void ConfigureGraphQLSchemaServices(IServiceCollection services); + + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { + if (env.IsDevelopment()) { + app.UseDeveloperExceptionPage(); + } + + app.UseWebSockets(); + + ConfigureGraphQLSchema(app); + + app.UseGraphiQLServer(new GraphiQLOptions { + GraphiQLPath = "/ui/graphiql", + GraphQLEndPoint = "/graphql" + }); + app.UseGraphQLVoyager(new GraphQLVoyagerOptions() { + GraphQLEndPoint = "/graphql", + Path = "/ui/voyager" + }); + app.UseGraphQLPlayground(new GraphQLPlaygroundOptions { + Path = "/ui/playground" + }); + } + + public abstract void ConfigureGraphQLSchema(IApplicationBuilder app); + } +} diff --git a/tests/IntegrationTestServer/StartupChat.cs b/tests/IntegrationTestServer/StartupChat.cs new file mode 100644 index 00000000..37c205e7 --- /dev/null +++ b/tests/IntegrationTestServer/StartupChat.cs @@ -0,0 +1,27 @@ +using GraphQL.Server; +using IntegrationTestServer.ChatSchema; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace IntegrationTestServer { + public class StartupChat: Startup { + public StartupChat(IConfiguration configuration, IWebHostEnvironment environment): base(configuration, environment) { } + + public override void ConfigureGraphQLSchemaServices(IServiceCollection services) { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + } + + public override void ConfigureGraphQLSchema(IApplicationBuilder app) { + app.UseGraphQLWebSockets("/graphql"); + app.UseGraphQL("/graphql"); + } + } +} diff --git a/tests/IntegrationTestServer/StartupStarWars.cs b/tests/IntegrationTestServer/StartupStarWars.cs new file mode 100644 index 00000000..c62dc5f2 --- /dev/null +++ b/tests/IntegrationTestServer/StartupStarWars.cs @@ -0,0 +1,31 @@ +using GraphQL.Server; +using GraphQL.StarWars; +using GraphQL.StarWars.Types; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace IntegrationTestServer { + public class StartupStarWars: Startup { + public StartupStarWars(IConfiguration configuration, IWebHostEnvironment environment): base(configuration, environment) { } + + public override void ConfigureGraphQLSchemaServices(IServiceCollection services) { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + } + + public override void ConfigureGraphQLSchema(IApplicationBuilder app) { + app.UseGraphQLWebSockets("/graphql"); + app.UseGraphQL("/graphql"); + } + } +}