Skip to content

Commit 8d8abaa

Browse files
committed
# Conflicts: # FlurlGraphQL.Querying/GraphQL/GraphQLQueryResults.cs
2 parents 864bf47 + 18ae608 commit 8d8abaa

15 files changed

+676
-95
lines changed

FlurlGraphQL.Querying/Flurl/FlurlGraphQLRequest.cs

Lines changed: 174 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,28 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Collections.ObjectModel;
4+
using System.Linq;
45
using System.Net;
56
using System.Net.Http;
67
using System.Threading;
78
using System.Threading.Tasks;
89
using Flurl;
910
using Flurl.Http;
1011
using Flurl.Http.Configuration;
12+
using Flurl.Http.Content;
1113
using Flurl.Util;
14+
using Newtonsoft.Json;
15+
using NullValueHandling = Flurl.NullValueHandling;
1216

1317
namespace 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

FlurlGraphQL.Querying/Flurl/FlurlGraphQLRequestExtensions.cs

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,48 @@ public static class FlurlGraphQLRequestExtensions
2020

2121
public static IFlurlGraphQLRequest ToGraphQLRequest(this IFlurlRequest request)
2222
=> request is FlurlGraphQLRequest graphqlRequest ? graphqlRequest : new FlurlGraphQLRequest(request);
23-
23+
24+
#endregion
25+
26+
#region WithGraphQLPersistedQuery()
27+
28+
/// <summary>
29+
/// Initialize the query body for a GraphQL query request.
30+
/// </summary>
31+
/// <param name="url"></param>
32+
/// <param name="query"></param>
33+
/// <returns>Returns an IFlurlGraphQLRequest for ready to chain for further initialization or execution.</returns>
34+
public static IFlurlGraphQLRequest WithGraphQLPersistedQuery(this string url, string id) => ToGraphQLRequest(url).WithGraphQLPersistedQuery(id);
35+
36+
/// <summary>
37+
/// Initialize the query body for a GraphQL query request.
38+
/// </summary>
39+
/// <param name="url"></param>
40+
/// <param name="query"></param>
41+
/// <returns>Returns an IFlurlGraphQLRequest for ready to chain for further initialization or execution.</returns>
42+
public static IFlurlGraphQLRequest WithGraphQLPersistedQuery(this Uri url, string id) => ToGraphQLRequest(url).WithGraphQLPersistedQuery(id);
43+
44+
/// <summary>
45+
/// Initialize the query body for a GraphQL query request.
46+
/// </summary>
47+
/// <param name="url"></param>
48+
/// <param name="query"></param>
49+
/// <returns>Returns an IFlurlGraphQLRequest for ready to chain for further initialization or execution.</returns>
50+
public static IFlurlGraphQLRequest WithGraphQLPersistedQuery(this Url url, string id) => ToGraphQLRequest(url).WithGraphQLPersistedQuery(id);
51+
52+
/// <summary>
53+
/// Initialize the query body for a GraphQL query request.
54+
/// </summary>
55+
/// <param name="request"></param>
56+
/// <param name="query"></param>
57+
/// <returns>Returns an IFlurlGraphQLRequest for ready to chain for further initialization or execution.</returns>
58+
public static IFlurlGraphQLRequest WithGraphQLPersistedQuery(this IFlurlRequest request, string id) => request.ToGraphQLRequest().WithGraphQLPersistedQuery(id);
59+
60+
2461
#endregion
25-
62+
2663
#region WithGraphQLQuery()...
27-
64+
2865
/// <summary>
2966
/// Initialize the query body for a GraphQL query request.
3067
/// </summary>
@@ -195,7 +232,7 @@ public static IFlurlGraphQLRequest SetGraphQLNewtonsoftJsonSerializerSettings(th
195232
#region PostGraphQLQueryAsnc()...
196233

197234
/// <summary>
198-
/// Execute the GraphQL Query, along with initialized variables, with the GraphQL Server specified by the original Url.
235+
/// Execute the GraphQL Query as a POST request (Strongly Recommended vs GET), along with initialized variables, with the GraphQL Server specified by the original Url.
199236
/// If no GraphQL Query has been specified via WithGraphQLQuery() then an InvalidOperationException will be thrown; otherwise
200237
/// any other errors returned by the GraphQL server will result in a FlurlGraphQLException being thrown containing all relevant details about the errors.
201238
/// </summary>
@@ -205,6 +242,19 @@ public static IFlurlGraphQLRequest SetGraphQLNewtonsoftJsonSerializerSettings(th
205242
public static Task<IFlurlGraphQLResponse> PostGraphQLQueryAsync(this IFlurlRequest request, CancellationToken cancellationToken = default)
206243
=> request.ToGraphQLRequest().PostGraphQLQueryAsync<object>(null, cancellationToken: cancellationToken);
207244

245+
/// <summary>
246+
/// STRONGLY DISCOURAGED -- Execute the GraphQL Query as a GET request, along with initialized variables, with the GraphQL Server specified by the original Url.
247+
/// This is Strongly Discouraged as POST requests are much more robust. But this is provided for edge cases where GET requests must be used.
248+
/// If no GraphQL Query has been specified via WithGraphQLQuery() then an InvalidOperationException will be thrown; otherwise
249+
/// any other errors returned by the GraphQL server will result in a FlurlGraphQLException being thrown containing all relevant details about the errors.
250+
/// </summary>
251+
/// <param name="request"></param>
252+
/// <param name="cancellationToken"></param>
253+
/// <returns>Returns an async IFlurlGraphQLResponse ready to be processed by various ReceiveGraphQL*() methods to handle the results based on the type of query.</returns>
254+
public static Task<IFlurlGraphQLResponse> GetGraphQLQueryAsync(this IFlurlRequest request, CancellationToken cancellationToken = default)
255+
=> request.ToGraphQLRequest().GetGraphQLQueryAsync<object>(null, cancellationToken: cancellationToken);
256+
257+
208258
#endregion
209259
}
210260
}

FlurlGraphQL.Querying/Flurl/FlurlGraphQLResponseExtensions.Internal.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,15 @@ internal static IGraphQLQueryResults<TEntityResult> ParseJsonToGraphQLResultsInt
212212
var jsonSerializer = JsonSerializer.CreateDefault(jsonSerializerSettings);
213213
jsonSerializer.Converters.Add(new GraphQLPageResultsToICollectionConverter());
214214

215+
return ParseJsonToGraphQLResultsWithJsonSerializerInternal<TEntityResult>(json, jsonSerializer);
216+
}
217+
218+
internal static IGraphQLQueryResults<TEntityResult> ParseJsonToGraphQLResultsWithJsonSerializerInternal<TEntityResult>(this JToken json, JsonSerializer jsonSerializer)
219+
where TEntityResult : class
220+
{
221+
if (json == null)
222+
return new GraphQLQueryResults<TEntityResult>();
223+
215224
//Dynamically parse the data from the results...
216225
//NOTE: We process PageInfo as Cursor Paging as the Default (because it's strongly encouraged by GraphQL.org
217226
// & Offset Paging model is a subset of Cursor Paging (less flexible).

FlurlGraphQL.Querying/Flurl/FlurlGraphQLResponseExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ public static async Task<IList<IGraphQLConnectionResults<TResult>>> ReceiveAllGr
149149
/// See: https://relay.dev/graphql/connections.htm
150150
/// </summary>
151151
/// <typeparam name="TResult"></typeparam>
152-
/// <param name="responseTask"></param>
152+
/// <param name="response"></param>
153153
/// <param name="queryOperationName"></param>
154154
/// <param name="cancellationToken"></param>
155155
/// <returns>Returns a List of ALL IGraphQLQueryConnectionResult set of typed results along with paging information returned by the query.</returns>

0 commit comments

Comments
 (0)