11using System ;
22using System . Collections . Generic ;
33using System . Collections . ObjectModel ;
4+ using System . Linq ;
45using System . Net ;
56using System . Net . Http ;
67using System . Threading ;
78using System . Threading . Tasks ;
89using Flurl ;
910using Flurl . Http ;
1011using Flurl . Http . Configuration ;
12+ using Flurl . Http . Content ;
1113using Flurl . Util ;
14+ using Newtonsoft . Json ;
15+ using NullValueHandling = Flurl . NullValueHandling ;
1216
1317namespace FlurlGraphQL . Querying
1418{
19+ public enum GraphQLQueryType
20+ {
21+ Undefined ,
22+ Query ,
23+ PersistedQuery
24+ } ;
25+
1526 public class FlurlGraphQLRequest : IFlurlGraphQLRequest
1627 {
1728 protected IFlurlRequest BaseFlurlRequest { get ; set ; }
@@ -20,6 +31,9 @@ internal FlurlGraphQLRequest(IFlurlRequest baseRequest)
2031 BaseFlurlRequest = baseRequest . AssertArgIsNotNull ( nameof ( baseRequest ) ) ;
2132 }
2233
34+ public GraphQLQueryType GraphQLQueryType { get ; protected set ; }
35+
36+
2337 #region GraphQL Variables
2438
2539 protected Dictionary < string , object > GraphQLVariablesInternal { get ; set ; } = new Dictionary < string , object > ( ) ;
@@ -93,62 +107,194 @@ public IFlurlGraphQLRequest SetContextItems(IEnumerable<(string Key, object Valu
93107
94108 #endregion
95109
96- #region GraphQL Query Param/Body
110+ #region WithGraphQLQuery()
97111
98112 public string GraphQLQuery { get ; protected set ; } = null ;
99113
100114 public IFlurlGraphQLRequest WithGraphQLQuery ( string query , NullValueHandling nullValueHandling = NullValueHandling . Remove )
101115 {
102116 if ( query != null || nullValueHandling == NullValueHandling . Remove )
117+ {
118+ //NOTE: By design, Persisted Queries and normal Queries are mutually exclusive so only one will be populated at a time,
119+ // we enforce this by clearing them both when a new item is being set...
120+ ClearGraphQLQuery ( ) ;
103121 GraphQLQuery = query ;
122+ GraphQLQueryType = GraphQLQueryType . Query ;
123+ }
104124
105125 return this ;
106126 }
107127
128+ #endregion
129+
130+ #region WithGraphQLPersistedQuery()
131+
132+ public IFlurlGraphQLRequest WithGraphQLPersistedQuery ( string id , NullValueHandling nullValueHandling = NullValueHandling . Remove )
133+ {
134+ if ( id != null || nullValueHandling == NullValueHandling . Remove )
135+ {
136+ //NOTE: By design, Persisted Queries and normal Queries are mutually exclusive so only one will be populated at a time,
137+ // we enforce this by clearing them both when a new item is being set...
138+ ClearGraphQLQuery ( ) ;
139+ GraphQLQuery = id ;
140+ GraphQLQueryType = GraphQLQueryType . PersistedQuery ;
141+ }
142+
143+ return this ;
144+ }
145+
146+ #endregion
147+
148+ #region ClearGraphQLQuery(), Clone()
149+
108150 public IFlurlGraphQLRequest ClearGraphQLQuery ( )
109151 {
110152 GraphQLQuery = null ;
153+ GraphQLQueryType = GraphQLQueryType . Undefined ;
111154 return this ;
112155 }
113156
114157 public IFlurlGraphQLRequest Clone ( )
115158 {
116- return new FlurlGraphQLRequest ( this . BaseFlurlRequest )
117- . WithGraphQLQuery ( this . GraphQLQuery )
159+ var clone = ( IFlurlGraphQLRequest ) new FlurlGraphQLRequest ( this . BaseFlurlRequest ) ;
160+
161+ switch ( this . GraphQLQueryType )
162+ {
163+ case GraphQLQueryType . PersistedQuery :
164+ clone = clone . WithGraphQLPersistedQuery ( this . GraphQLQuery ) ;
165+ break ;
166+ case GraphQLQueryType . Query :
167+ clone = clone . WithGraphQLQuery ( this . GraphQLQuery ) ;
168+ break ;
169+ //NOTE: It's ok to Clone a GraphQL query that you may be initialized later...
170+ //default:
171+ // throw new ArgumentOutOfRangeException(nameof(GraphQLQueryType), "The GraphQL Query Type is undefined or invalid.");
172+ }
173+
174+ clone
118175 . SetGraphQLVariables ( this . GraphQLVariablesInternal )
119176 . SetContextItems ( this . ContextBagInternal ) ;
177+
178+ return clone ;
120179 }
121180
122181 #endregion
123182
124183 #region GraphQL Query Execution with Server
125184
126- public async Task < IFlurlGraphQLResponse > PostGraphQLQueryAsync < TVariables > ( TVariables variables , CancellationToken cancellationToken = default , NullValueHandling nullValueHandling = NullValueHandling . Remove )
127- where TVariables : class
185+ /// <summary>
186+ /// Execute the GraphQL query with the Server using POST request (Strongly Recommended vs Get).
187+ /// </summary>
188+ /// <typeparam name="TVariables"></typeparam>
189+ /// <param name="variables"></param>
190+ /// <param name="cancellationToken"></param>
191+ /// <param name="nullValueHandling"></param>
192+ /// <returns></returns>
193+ /// <exception cref="InvalidOperationException"></exception>
194+ /// <exception cref="FlurlGraphQLException"></exception>
195+ public async Task < IFlurlGraphQLResponse > PostGraphQLQueryAsync < TVariables > (
196+ TVariables variables ,
197+ CancellationToken cancellationToken = default ,
198+ NullValueHandling nullValueHandling = NullValueHandling . Remove
199+ ) where TVariables : class
128200 {
129- var graphqlQuery = this . GraphQLQuery ;
201+ //NOTE: By design, Persisted Queries and normal Queries are mutually exclusive so only one will be populated at a time...
202+ var graphqlQueryType = this . GraphQLQueryType ;
203+ var graphqlQueryOrId = this . GraphQLQuery ;
130204
131205 //Get the GraphQL Query and remove it from the QueryString...
132- if ( string . IsNullOrWhiteSpace ( graphqlQuery ) )
133- throw new InvalidOperationException ( $ "The GraphQL Query is undefined; use { nameof ( WithGraphQLQuery ) } () to specify the body of the query.") ;
206+ if ( graphqlQueryType == GraphQLQueryType . Undefined || string . IsNullOrWhiteSpace ( graphqlQueryOrId ) )
207+ throw new InvalidOperationException ( $ "The GraphQL Query is undefined; use { nameof ( WithGraphQLQuery ) } () or { nameof ( WithGraphQLPersistedQuery ) } () to specify the query.") ;
134208
135209 //Process any additional variables that may have been provided directly to this call...
136210 //NOTE: None of these will have used our prefix convention...
137211 if ( variables != null )
138212 this . SetGraphQLVariables ( variables , nullValueHandling ) ;
139213
140- //Execute the Query with the GraphQL Server...
141- var graphqlPayload = new FlurlGraphQLRequestPayload ( graphqlQuery , this . GraphQLVariablesInternal ) ;
214+ //Execute the Request with shared Exception handling...
215+ return await ExecuteRequestWithExceptionHandling ( async ( ) =>
216+ {
217+ //Execute the Query with the GraphQL Server...
218+ var graphqlPayload = new FlurlGraphQLRequestPayload ( graphqlQueryType , graphqlQueryOrId , this . GraphQLVariablesInternal ) ;
219+ var jsonPayload = SerializeToJsonWithOptionalGraphQLSerializerSettings ( graphqlPayload ) ;
220+
221+ //Since we have our own GraphQL Serializer Settings, our payload is already serialized so we can just send it!
222+ //NOTE: Borrowed directly from the Flurl.PostJsonAsync() method but
223+ var response = await this . SendAsync (
224+ HttpMethod . Post ,
225+ new CapturedJsonContent ( jsonPayload ) ,
226+ cancellationToken ,
227+ completionOption : HttpCompletionOption . ResponseContentRead
228+ ) . ConfigureAwait ( false ) ;
142229
143- try
230+ return new FlurlGraphQLResponse ( response , this ) ;
231+
232+ } ) . ConfigureAwait ( false ) ;
233+ }
234+
235+ /// <summary>
236+ /// STRONGLY DISCOURAGED -- Execute the GraphQL query with the Server using GET request.
237+ /// This is Strongly Discouraged as POST requests are much more robust. But this is provided for edge cases where GET requests must be used.
238+ /// </summary>
239+ /// <typeparam name="TVariables"></typeparam>
240+ /// <param name="variables"></param>
241+ /// <param name="cancellationToken"></param>
242+ /// <param name="nullValueHandling"></param>
243+ /// <returns></returns>
244+ /// <exception cref="InvalidOperationException"></exception>
245+ /// <exception cref="FlurlGraphQLException"></exception>
246+ public async Task < IFlurlGraphQLResponse > GetGraphQLQueryAsync < TVariables > (
247+ TVariables variables ,
248+ CancellationToken cancellationToken = default ,
249+ NullValueHandling nullValueHandling = NullValueHandling . Remove
250+ ) where TVariables : class
251+ {
252+ //NOTE: By design, Persisted Queries and normal Queries are mutually exclusive so only one will be populated at a time...
253+ var graphqlQueryType = this . GraphQLQueryType ;
254+ var graphqlQueryOrId = this . GraphQLQuery ;
255+
256+ //Get the GraphQL Query and remove it from the QueryString...
257+ if ( graphqlQueryType == GraphQLQueryType . Undefined || string . IsNullOrWhiteSpace ( graphqlQueryOrId ) )
258+ throw new InvalidOperationException ( $ "The GraphQL Query is undefined; use { nameof ( WithGraphQLQuery ) } () or { nameof ( WithGraphQLPersistedQuery ) } () to specify the query.") ;
259+
260+ //Process any additional variables that may have been provided directly to this call...
261+ //NOTE: None of these will have used our prefix convention...
262+ if ( variables != null )
263+ this . SetGraphQLVariables ( variables , nullValueHandling ) ;
264+
265+ //Execute the Request with shared Exception handling...
266+ return await ExecuteRequestWithExceptionHandling ( async ( ) =>
144267 {
145- var response = await this . PostJsonAsync (
146- graphqlPayload ,
268+ switch ( this . GraphQLQueryType )
269+ {
270+ case GraphQLQueryType . Query : this . SetQueryParam ( "query" , this . GraphQLQuery ) ; break ;
271+ case GraphQLQueryType . PersistedQuery : this . SetQueryParam ( "id" , this . GraphQLQuery ) ; break ;
272+ default : throw new ArgumentOutOfRangeException ( nameof ( graphqlQueryType ) , $ "GraphQL Query Type [{ graphqlQueryType } ] cannot be initialized.") ;
273+ }
274+
275+ if ( this . GraphQLVariablesInternal ? . Any ( ) ?? false )
276+ {
277+ var variablesJson = SerializeToJsonWithOptionalGraphQLSerializerSettings ( this . GraphQLVariablesInternal ) ;
278+ this . SetQueryParam ( "variables" , variablesJson ) ;
279+ }
280+
281+ var response = await this . GetAsync (
147282 cancellationToken ,
148283 completionOption : HttpCompletionOption . ResponseContentRead
149284 ) . ConfigureAwait ( false ) ;
150285
151286 return new FlurlGraphQLResponse ( response , this ) ;
287+
288+ } ) . ConfigureAwait ( false ) ;
289+ }
290+
291+ protected async Task < FlurlGraphQLResponse > ExecuteRequestWithExceptionHandling ( Func < Task < FlurlGraphQLResponse > > sendRequestFunc )
292+ {
293+ sendRequestFunc . AssertArgIsNotNull ( nameof ( sendRequestFunc ) ) ;
294+
295+ try
296+ {
297+ return await sendRequestFunc ( ) . ConfigureAwait ( false ) ;
152298 }
153299 catch ( FlurlHttpException httpException )
154300 {
@@ -159,13 +305,27 @@ public async Task<IFlurlGraphQLResponse> PostGraphQLQueryAsync<TVariables>(TVari
159305 throw new FlurlGraphQLException (
160306 $ "[{ ( int ) HttpStatusCode . BadRequest } -{ HttpStatusCode . BadRequest } ] The GraphQL server returned a bad request response for the query."
161307 + " This is likely caused by a malformed, or non-parsable query; validate the query syntax, operation name, arguments, etc."
162- + " to ensure that the query is valid." , graphqlQuery , errorContent , httpStatusCode , httpException
308+ + " to ensure that the query is valid." , this . GraphQLQuery , errorContent , httpStatusCode , httpException
163309 ) ;
164310 else
165311 throw ;
166312 }
167313 }
168314
315+ protected string SerializeToJsonWithOptionalGraphQLSerializerSettings ( object obj )
316+ {
317+ var jsonSerializerSettings = ContextBag ? . TryGetValue ( nameof ( JsonSerializerSettings ) , out var serializerSettings ) ?? false
318+ ? serializerSettings as JsonSerializerSettings
319+ : null ;
320+
321+ //Ensure that all json parsing uses a Serializer with the GraphQL Contract Resolver...
322+ //NOTE: We still support normal Serializer Default settings via Newtonsoft framework!
323+ //var jsonSerializer = JsonSerializer.CreateDefault(jsonSerializerSettings);
324+ var json = JsonConvert . SerializeObject ( obj , jsonSerializerSettings ) ;
325+ return json ;
326+ }
327+
328+
169329 #endregion
170330
171331 #region IFlurlRequest Interface Implementations
0 commit comments