Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit6236c9b

Browse files
sungam3rrose-aShane32
authored
Add APQ support (#555)
* Add APQ support* changes* rem* note* progress* progress* fix variable name* move APQ code to SendQueryAsync method to allow usage over websocket, too* make the APQDisabledForSession flag public (helps for testing)* create a test that uses the APQ feature* test APQ with websocket transport* move code for generation of the APQ extension into GraphQLRequest* fix naming* replace system.memory reference with narrower system.buffers reference* Update src/GraphQL.Primitives/GraphQLRequest.csCo-authored-by: Shane Krueger <shane@acdmail.com>* Update src/GraphQL.Primitives/GraphQLRequest.csCo-authored-by: Shane Krueger <shane@acdmail.com>* document APQ feature +semver: feature* optimize docs---------Co-authored-by: Alexander Rose <arose@haprotec.de>Co-authored-by: Alexander Rose <alex@rose-a.de>Co-authored-by: Shane Krueger <shane@acdmail.com>
1 parentdbd9c20 commit6236c9b

File tree

13 files changed

+315
-32
lines changed

13 files changed

+315
-32
lines changed

‎GraphQL.Client.sln.DotSettings‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
<wpf:ResourceDictionaryxml:space="preserve"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:s="clr-namespace:System;assembly=mscorlib"xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml"xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
2+
<s:Stringx:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=APQ/@EntryIndexedValue">APQ</s:String>
23
<s:Stringx:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=QL/@EntryIndexedValue">QL</s:String></wpf:ResourceDictionary>

‎README.md‎

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ The Library will try to follow the following standards and documents:
2222

2323
##Usage
2424

25+
The intended use of`GraphQLHttpClient` is to keep one instance alive per endpoint (obvious in case you're
26+
operating full websocket, but also true for regular requests) and is built with thread-safety in mind.
27+
2528
###Create a GraphQLHttpClient
2629

2730
```csharp
@@ -159,17 +162,22 @@ var subscription = subscriptionStream.Subscribe(response =>
159162
subscription.Dispose();
160163
```
161164

162-
##Syntax Highlighting for GraphQL strings in IDEs
165+
###Automatic persisted queries (APQ)
163166

164-
.NET 7.0 introduced the[StringSyntaxAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.stringsyntaxattribute?view=net-8.0) to have a unified way of telling what data is expected in a given`string` or`ReadOnlySpan<char>`. IDEs like Visual Studio and Rider can then use this to provide syntax highlighting and checking.
167+
[Automatic persisted queries (APQ)](https://www.apollographql.com/docs/apollo-server/performance/apq/) are supported since client version 6.1.0.
165168

166-
From v6.0.4 on all GraphQL string parameters in this library are decorated with the`[StringSyntax("GraphQL")]` attribute.
169+
APQ can be enabled by configuring`GraphQLHttpClientOptions.EnableAutomaticPersistedQueries` to resolve to`true`.
167170

168-
Currently, there is no native support for GraphQL formatting and syntax highlighting in Visual Studio, but the[GraphQLTools Extension](https://marketplace.visualstudio.com/items?itemName=codearchitects-research.GraphQLTools) provides that for you.
171+
By default, the client will automatically disable APQ for the current session if the server responds with a`PersistedQueryNotSupported` error or a 400 or 600 HTTP status code.
172+
This can be customized by configuring`GraphQLHttpClientOptions.DisableAPQ`.
169173

170-
For Rider, JetBrains provides a[Plugin](https://plugins.jetbrains.com/plugin/8097-graphql), too.
174+
To re-enable APQ after it has been automatically disabled,`GraphQLHttpClient` needs to be disposed an recreated.
171175

172-
To leverage syntax highlighting in variable declarations, the`GraphQLQuery` value record type is provided:
176+
APQ works by first sending a hash of the query string to the server, and only sending the full query string if the server has not yet cached a query with a matching hash.
177+
With queries supplied as a string parameter to`GraphQLRequest`, the hash gets computed each time the request is sent.
178+
179+
When you want to reuse a query string (propably to leverage APQ:wink:), declare the query using the`GraphQLQuery` class. This way, the hash gets computed once on construction
180+
of the`GraphQLQuery` object and handed down to each`GraphQLRequest` using the query.
173181

174182
```csharp
175183
GraphQLQueryquery=new("""
@@ -191,6 +199,19 @@ var graphQLResponse = await graphQLClient.SendQueryAsync<ResponseType>(
191199
new {id="cGVvcGxlOjE=" });
192200
```
193201

202+
###Syntax Highlighting for GraphQL strings in IDEs
203+
204+
.NET 7.0 introduced the[StringSyntaxAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.stringsyntaxattribute?view=net-8.0) to have a unified way of telling what data is expected in a given`string` or`ReadOnlySpan<char>`. IDEs like Visual Studio and Rider can then use this to provide syntax highlighting and checking.
205+
206+
From v6.0.4 on all GraphQL string parameters in this library are decorated with the`[StringSyntax("GraphQL")]` attribute.
207+
208+
Currently, there is no native support for GraphQL formatting and syntax highlighting in Visual Studio, but the[GraphQLTools Extension](https://marketplace.visualstudio.com/items?itemName=codearchitects-research.GraphQLTools) provides that for you.
209+
210+
For Rider, JetBrains provides a[Plugin](https://plugins.jetbrains.com/plugin/8097-graphql), too.
211+
212+
To leverage syntax highlighting in variable declarations, use the`GraphQLQuery` class.
213+
214+
194215
##Useful Links
195216

196217
*[StarWars Example Server (GitHub)](https://github.com/graphql/swapi-graphql)

‎src/GraphQL.Client.Abstractions/GraphQLClientExtensions.cs‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,12 @@ public static Task<GraphQLResponse<TResponse>> SendQueryAsync<TResponse>(this IG
1313
cancellationToken:cancellationToken);
1414
}
1515

16-
#ifNET6_0_OR_GREATER
1716
publicstaticTask<GraphQLResponse<TResponse>>SendQueryAsync<TResponse>(thisIGraphQLClientclient,
1817
GraphQLQueryquery,object?variables=null,
1918
string?operationName=null,Func<TResponse>?defineResponseType=null,
2019
CancellationTokencancellationToken=default)
2120
=>SendQueryAsync(client,query.Text,variables,operationName,defineResponseType,
2221
cancellationToken);
23-
#endif
2422

2523
publicstaticTask<GraphQLResponse<TResponse>>SendMutationAsync<TResponse>(thisIGraphQLClientclient,
2624
[StringSyntax("GraphQL")]stringquery,object?variables=null,
@@ -31,13 +29,11 @@ public static Task<GraphQLResponse<TResponse>> SendMutationAsync<TResponse>(this
3129
cancellationToken:cancellationToken);
3230
}
3331

34-
#ifNET6_0_OR_GREATER
3532
publicstaticTask<GraphQLResponse<TResponse>>SendMutationAsync<TResponse>(thisIGraphQLClientclient,
3633
GraphQLQueryquery,object?variables=null,string?operationName=null,Func<TResponse>?defineResponseType=null,
3734
CancellationTokencancellationToken=default)
3835
=>SendMutationAsync(client,query.Text,variables,operationName,defineResponseType,
3936
cancellationToken);
40-
#endif
4137

4238
publicstaticTask<GraphQLResponse<TResponse>>SendQueryAsync<TResponse>(thisIGraphQLClientclient,
4339
GraphQLRequestrequest,Func<TResponse>defineResponseType,CancellationTokencancellationToken=default)

‎src/GraphQL.Client/GraphQLHttpClient.cs‎

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ public class GraphQLHttpClient : IGraphQLWebSocketClient, IDisposable
1717
privatereadonlyCancellationTokenSource_cancellationTokenSource=new();
1818

1919
privatereadonlybool_disposeHttpClient=false;
20-
2120
/// <summary>
2221
/// the json serializer
2322
/// </summary>
@@ -33,6 +32,12 @@ public class GraphQLHttpClient : IGraphQLWebSocketClient, IDisposable
3332
/// </summary>
3433
publicGraphQLHttpClientOptionsOptions{get;}
3534

35+
/// <summary>
36+
/// This flag is set to <see langword="true"/> when an error has occurred on an APQ and <see cref="GraphQLHttpClientOptions.DisableAPQ"/>
37+
/// has returned <see langword="true"/>. To reset this, the instance of <see cref="GraphQLHttpClient"/> has to be disposed and a new one must be created.
38+
/// </summary>
39+
publicboolAPQDisabledForSession{get;privateset;}
40+
3641
/// <inheritdoc />
3742
publicIObservable<Exception>WebSocketReceiveErrors=>GraphQlHttpWebSocket.ReceiveErrors;
3843

@@ -84,12 +89,49 @@ public GraphQLHttpClient(string endPoint, IGraphQLWebsocketJsonSerializer serial
8489

8590
#region IGraphQLClient
8691

92+
privateconstintAPQ_SUPPORTED_VERSION=1;
93+
8794
/// <inheritdoc />
8895
publicasyncTask<GraphQLResponse<TResponse>>SendQueryAsync<TResponse>(GraphQLRequestrequest,CancellationTokencancellationToken=default)
8996
{
90-
returnOptions.UseWebSocketForQueriesAndMutations||Options.WebSocketEndPointis notnull&&Options.EndPointisnull||Options.EndPoint.HasWebSocketScheme()
91-
?awaitGraphQlHttpWebSocket.SendRequestAsync<TResponse>(request,cancellationToken).ConfigureAwait(false)
92-
:awaitSendHttpRequestAsync<TResponse>(request,cancellationToken).ConfigureAwait(false);
97+
cancellationToken.ThrowIfCancellationRequested();
98+
99+
string?savedQuery=null;
100+
booluseAPQ=false;
101+
102+
if(request.Query!=null&&!APQDisabledForSession&&Options.EnableAutomaticPersistedQueries(request))
103+
{
104+
// https://www.apollographql.com/docs/react/api/link/persisted-queries/
105+
useAPQ=true;
106+
request.GeneratePersistedQueryExtension();
107+
savedQuery=request.Query;
108+
request.Query=null;
109+
}
110+
111+
varresponse=awaitSendQueryInternalAsync<TResponse>(request,cancellationToken);
112+
113+
if(useAPQ)
114+
{
115+
if(response.Errors?.Any(error=>string.Equals(error.Message,"PersistedQueryNotFound",StringComparison.CurrentCultureIgnoreCase))==true)
116+
{
117+
// GraphQL server supports APQ!
118+
119+
// Alas, for the first time we did not guess and in vain removed Query, so we return Query and
120+
// send request again. This is one-time "cache miss", not so scary.
121+
request.Query=savedQuery;
122+
returnawaitSendQueryInternalAsync<TResponse>(request,cancellationToken);
123+
}
124+
else
125+
{
126+
// GraphQL server either supports APQ of some other version, or does not support it at all.
127+
// Send a request for the second time. This is better than returning an error. Let the client work with APQ disabled.
128+
APQDisabledForSession=Options.DisableAPQ(response);
129+
request.Query=savedQuery;
130+
returnawaitSendQueryInternalAsync<TResponse>(request,cancellationToken);
131+
}
132+
}
133+
134+
returnresponse;
93135
}
94136

95137
/// <inheritdoc />
@@ -123,6 +165,10 @@ public IObservable<GraphQLResponse<TResponse>> CreateSubscriptionStream<TRespons
123165
publicTaskSendPongAsync(object?payload)=>GraphQlHttpWebSocket.SendPongAsync(payload);
124166

125167
#region Private Methods
168+
privateasyncTask<GraphQLResponse<TResponse>>SendQueryInternalAsync<TResponse>(GraphQLRequestrequest,CancellationTokencancellationToken=default)=>
169+
Options.UseWebSocketForQueriesAndMutations||Options.WebSocketEndPointis notnull&&Options.EndPointisnull||Options.EndPoint.HasWebSocketScheme()
170+
?awaitGraphQlHttpWebSocket.SendRequestAsync<TResponse>(request,cancellationToken).ConfigureAwait(false)
171+
:awaitSendHttpRequestAsync<TResponse>(request,cancellationToken).ConfigureAwait(false);
126172

127173
privateasyncTask<GraphQLHttpResponse<TResponse>>SendHttpRequestAsync<TResponse>(GraphQLRequestrequest,CancellationTokencancellationToken=default)
128174
{

‎src/GraphQL.Client/GraphQLHttpClientOptions.cs‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public class GraphQLHttpClientOptions
2525
publicUri?WebSocketEndPoint{get;set;}
2626

2727
/// <summary>
28-
/// The GraphQL websocket protocol to be used. Defaults to the older "graphql-ws" protocol to not break old code.
28+
/// The GraphQL websocket protocol to be used. Defaults to the older "graphql-ws" protocol to not break old code.
2929
/// </summary>
3030
publicstring?WebSocketProtocol{get;set;}=WebSocketProtocols.AUTO_NEGOTIATE;
3131

@@ -99,4 +99,21 @@ public static bool DefaultIsValidResponseToDeserialize(HttpResponseMessage r)
9999
/// </summary>
100100
publicProductInfoHeaderValue?DefaultUserAgentRequestHeader{get;set;}
101101
=newProductInfoHeaderValue(typeof(GraphQLHttpClient).Assembly.GetName().Name,typeof(GraphQLHttpClient).Assembly.GetName().Version.ToString());
102+
103+
/// <summary>
104+
/// Delegate permitting use of <see href="https://www.apollographql.com/docs/react/api/link/persisted-queries/">Automatic Persisted Queries (APQ)</see>.
105+
/// By default, returns <see langword="false" /> for all requests. Note that GraphQL server should support APQ. Otherwise, the client disables APQ completely
106+
/// after an unsuccessful attempt to send an APQ request and then send only regular requests.
107+
/// </summary>
108+
publicFunc<GraphQLRequest,bool>EnableAutomaticPersistedQueries{get;set;}= _=>false;
109+
110+
/// <summary>
111+
/// A delegate which takes an <see cref="IGraphQLResponse"/> and returns a boolean to disable any future persisted queries for that session.
112+
/// This defaults to disabling on PersistedQueryNotSupported or a 400 or 500 HTTP error.
113+
/// </summary>
114+
publicFunc<IGraphQLResponse,bool>DisableAPQ{get;set;}= response=>
115+
{
116+
returnresponse.Errors?.Any(error=>string.Equals(error.Message,"PersistedQueryNotSupported",StringComparison.CurrentCultureIgnoreCase))==true
117+
||responseisIGraphQLHttpResponsehttpResponse&&(int)httpResponse.StatusCode>=400&&(int)httpResponse.StatusCode<600;
118+
};
102119
}

‎src/GraphQL.Client/GraphQLHttpRequest.cs‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,10 @@ public GraphQLHttpRequest([StringSyntax("GraphQL")] string query, object? variab
1919
:base(query,variables,operationName,extensions)
2020
{
2121
}
22-
23-
#ifNET6_0_OR_GREATER
2422
publicGraphQLHttpRequest(GraphQLQueryquery,object?variables=null,string?operationName=null,Dictionary<string,object?>?extensions=null)
2523
:base(query,variables,operationName,extensions)
2624
{
2725
}
28-
#endif
2926

3027
publicGraphQLHttpRequest(GraphQLRequestother)
3128
:base(other)

‎src/GraphQL.Client/GraphQLHttpResponse.cs‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
namespaceGraphQL.Client.Http;
55

6-
publicclassGraphQLHttpResponse<T>:GraphQLResponse<T>
6+
publicclassGraphQLHttpResponse<T>:GraphQLResponse<T>,IGraphQLHttpResponse
77
{
88
publicGraphQLHttpResponse(GraphQLResponse<T>response,HttpResponseHeadersresponseHeaders,HttpStatusCodestatusCode)
99
{
@@ -19,6 +19,13 @@ public GraphQLHttpResponse(GraphQLResponse<T> response, HttpResponseHeaders resp
1919
publicHttpStatusCodeStatusCode{get;set;}
2020
}
2121

22+
publicinterfaceIGraphQLHttpResponse:IGraphQLResponse
23+
{
24+
HttpResponseHeadersResponseHeaders{get;set;}
25+
26+
HttpStatusCodeStatusCode{get;set;}
27+
}
28+
2229
publicstaticclassGraphQLResponseExtensions
2330
{
2431
publicstaticGraphQLHttpResponse<T>ToGraphQLHttpResponse<T>(thisGraphQLResponse<T>response,HttpResponseHeadersresponseHeaders,HttpStatusCodestatusCode)=>new(response,responseHeaders,statusCode);

‎src/GraphQL.Primitives/GraphQL.Primitives.csproj‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,7 @@
66
<TargetFrameworks>netstandard2.0;net6.0;net7.0;net8.0</TargetFrameworks>
77
</PropertyGroup>
88

9+
<ItemGroupCondition=" '$(TargetFramework)' == 'netstandard2.0'">
10+
<PackageReferenceInclude="System.Buffers"Version="4.5.1" />
11+
</ItemGroup>
912
</Project>
Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,34 @@
1-
#ifNET6_0_OR_GREATER
21
usingSystem.Diagnostics.CodeAnalysis;
3-
42
namespaceGraphQL;
53

64
/// <summary>
7-
/// Value record for a GraphQL query string
5+
/// Value object representing a GraphQL query string and storing the corresponding APQ hash. <br />
6+
/// Use this to hold query strings you want to use more than once.
87
/// </summary>
9-
/// <param name="Text">the actual query string</param>
10-
publicreadonlyrecordstructGraphQLQuery([StringSyntax("GraphQL")]stringText)
8+
publicclassGraphQLQuery:IEquatable<GraphQLQuery>
119
{
10+
/// <summary>
11+
/// The actual query string
12+
/// </summary>
13+
publicstringText{get;}
14+
15+
/// <summary>
16+
/// The SHA256 hash used for the automatic persisted queries feature (APQ)
17+
/// </summary>
18+
publicstringSha256Hash{get;}
19+
20+
publicGraphQLQuery([StringSyntax("GraphQL")]stringtext)
21+
{
22+
Text=text;
23+
Sha256Hash=Hash.Compute(Text);
24+
}
25+
1226
publicstaticimplicitoperatorstring(GraphQLQueryquery)
1327
=>query.Text;
14-
};
15-
#endif
28+
29+
publicboolEquals(GraphQLQueryother)=>Sha256Hash==other.Sha256Hash;
30+
31+
publicoverrideboolEquals(object?obj)=>objisGraphQLQueryother&&Equals(other);
32+
33+
publicoverrideintGetHashCode()=>Sha256Hash.GetHashCode();
34+
}

‎src/GraphQL.Primitives/GraphQLRequest.cs‎

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,28 @@ public class GraphQLRequest : Dictionary<string, object>, IEquatable<GraphQLRequ
1111
publicconststringQUERY_KEY="query";
1212
publicconststringVARIABLES_KEY="variables";
1313
publicconststringEXTENSIONS_KEY="extensions";
14+
publicconststringEXTENSIONS_PERSISTED_QUERY_KEY="persistedQuery";
15+
publicconstintAPQ_SUPPORTED_VERSION=1;
16+
17+
privatestring?_sha265Hash;
1418

1519
/// <summary>
16-
/// TheQuery
20+
/// Thequery string
1721
/// </summary>
1822
[StringSyntax("GraphQL")]
19-
publicstringQuery
23+
publicstring?Query
2024
{
2125
get=>TryGetValue(QUERY_KEY,outobjectvalue)?(string)value:null;
22-
set=>this[QUERY_KEY]=value;
26+
set
27+
{
28+
this[QUERY_KEY]=value;
29+
// if the query string gets overwritten, reset the hash value
30+
_sha265Hash=null;
31+
}
2332
}
2433

2534
/// <summary>
26-
/// Thename of the Operation
35+
/// Theoperation to execute
2736
/// </summary>
2837
publicstring?OperationName
2938
{
@@ -59,16 +68,28 @@ public GraphQLRequest([StringSyntax("GraphQL")] string query, object? variables
5968
Extensions=extensions;
6069
}
6170

62-
#ifNET6_0_OR_GREATER
6371
publicGraphQLRequest(GraphQLQueryquery,object?variables=null,string?operationName=null,
6472
Dictionary<string,object?>?extensions=null)
6573
:this(query.Text,variables,operationName,extensions)
6674
{
75+
_sha265Hash=query.Sha256Hash;
6776
}
68-
#endif
6977

7078
publicGraphQLRequest(GraphQLRequestother):base(other){}
7179

80+
publicvoidGeneratePersistedQueryExtension()
81+
{
82+
if(Queryisnull)
83+
thrownewInvalidOperationException($"{nameof(Query)} is null");
84+
85+
Extensions??=new();
86+
Extensions[EXTENSIONS_PERSISTED_QUERY_KEY]=newDictionary<string,object>
87+
{
88+
["version"]=APQ_SUPPORTED_VERSION,
89+
["sha256Hash"]=_sha265Hash??=Hash.Compute(Query),
90+
};
91+
}
92+
7293
/// <summary>
7394
/// Returns a value that indicates whether this instance is equal to a specified object
7495
/// </summary>

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp