Skip to content

Commit 84a1d6f

Browse files
committed
- Improve handling of Enums so that automatic processing as SCREAMING_CASE is handled now without the need to have [EnumMember("")] attributes on every enum value when the names match.
- Improve handling of GraphQL Serialization to now automatically use CamelCase for System.Text.Json & Newtonsoft.Json; it was already being handled when de-serializing but not when serializing. - Added new methods to help streamline the configuration of Json Serializer options/settings; a simple action/lambda can now be used to set Json Serialization Options/Settings.
1 parent b03160f commit 84a1d6f

21 files changed

+394
-131
lines changed

FlurlGraphQL.Newtonsoft/FlurlGraphQL.Newtonsoft.csproj

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
(because it had big issues using netstandard prior to .NET Framework 4.7) -->
66
<TargetFrameworks>net461;netstandard2.0;net6.0</TargetFrameworks>
77
<ImplicitUsings>disable</ImplicitUsings>
8-
<Version>2.0.1</Version>
9-
<AssemblyVersion>2.0.1</AssemblyVersion>
10-
<FileVersion>2.0.1</FileVersion>
8+
<Version>2.0.2</Version>
9+
<AssemblyVersion>2.0.2</AssemblyVersion>
10+
<FileVersion>2.0.2</FileVersion>
1111
<Authors>BBernard / CajunCoding</Authors>
1212
<Company>CajunCoding</Company>
1313
<Description>Newtonsoft JSON Supportt for FlurlGraphQL v2.0+ -- A GraphQL client extensions for Flurl.Http - lightweight, simplified, asynchronous, fluent GraphQL client API extensions for the amazing Flurl Http library!</Description>
@@ -18,9 +18,12 @@
1818
<RepositoryUrl>https://github.com/cajuncoding/FlurlGraphQL</RepositoryUrl>
1919
<PackageReleaseNotes>
2020
Release Notes:
21-
- Fix issue with incorrect deserialization when using wrapper convenience class GraphQLEdge&lt;T&gt;.
21+
- Improve handling of Enums so that automatic processing as SCREAMING_CASE is handled now without the need to have [EnumMember("")] attributes on every enum value when the names match.
22+
- Improve handling of GraphQL Serialization to now automatically use CamelCase for System.Text.Json &amp; Newtonsoft.Json; it was already being handled when de-serializing but not when serializing.
23+
- Added new methods to help streamline the configuration of Json Serializer options/settings; a simple action/lambda can now be used to set Json Serialization Options/Settings.
2224

2325
Prior Release Notes:
26+
- Fix issue with incorrect deserialization when using wrapper convenience class GraphQLEdge&lt;T&gt;.
2427
- Newtonsoft JSON Compatibility implementation for FlurlGraphQL v2.0 using the all new Flurl v4.0+.
2528
</PackageReleaseNotes>
2629
<PackageTags>graphql, graph-ql, graphql client, graphql query, flurl, newtonsoft, rest, http, http client, json, hotchocolate, paging, cursor</PackageTags>

FlurlGraphQL.Newtonsoft/FlurlGraphQLNewtonsoftJsonExtensions.cs

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
using System.Threading.Tasks;
55
using Flurl.Http;
66
using Flurl.Http.Newtonsoft;
7+
using FlurlGraphQL.JsonProcessing;
78
using Newtonsoft.Json;
89
using Newtonsoft.Json.Linq;
910

10-
namespace FlurlGraphQL.JsonProcessing
11+
//NOTE: To ensure these Extensions are readily discoverable we use the root FlurlGraphQL namespace!
12+
namespace FlurlGraphQL
1113
{
1214
public static class FlurlGraphQLNewtonsoftJsonExtensions
1315
{
@@ -41,16 +43,40 @@ public static IFlurlGraphQLRequest UseGraphQLNewtonsoftJson(this IFlurlRequest r
4143
/// <param name="graphqlRequest"></param>
4244
/// <param name="newtonsoftJsonSettings"></param>
4345
/// <returns>Returns an IFlurlGraphQLRequest for ready to chain for further initialization or execution.</returns>
44-
public static IFlurlGraphQLRequest UseGraphQLNewtonsoftJson(this IFlurlGraphQLRequest graphqlRequest, JsonSerializerSettings newtonsoftJsonSettings)
46+
public static IFlurlGraphQLRequest UseGraphQLNewtonsoftJson(this IFlurlGraphQLRequest graphqlRequest, JsonSerializerSettings newtonsoftJsonSettings = null)
4547
{
4648
if (graphqlRequest is FlurlGraphQLRequest flurlGraphQLRequest)
4749
{
48-
if (newtonsoftJsonSettings == null && flurlGraphQLRequest.GraphQLJsonSerializer is IFlurlGraphQLNewtonsoftJsonSerializer existingNewtonsoftJsonSerializer)
49-
flurlGraphQLRequest.GraphQLJsonSerializer = existingNewtonsoftJsonSerializer;
50-
else
51-
flurlGraphQLRequest.GraphQLJsonSerializer = new FlurlGraphQLNewtonsoftJsonSerializer(
52-
newtonsoftJsonSettings ?? FlurlGraphQLNewtonsoftJsonSerializer.CreateDefaultSerializerSettings()
53-
);
50+
flurlGraphQLRequest.GraphQLJsonSerializer = new FlurlGraphQLNewtonsoftJsonSerializer(
51+
newtonsoftJsonSettings ?? FlurlGraphQLNewtonsoftJsonSerializer.CreateDefaultSerializerSettings()
52+
);
53+
}
54+
55+
return graphqlRequest;
56+
}
57+
58+
/// <summary>
59+
/// Initialize a custom GraphQL Json Serializer using Newtonsoft.Json, but only for this GraphQL request; isolated from any other GraphQL Requests.
60+
/// </summary>
61+
/// <param name="request"></param>
62+
/// <param name="configureJsonSerializerSettings">Action method to configure the existing options as needed.</param>
63+
/// <returns>Returns an IFlurlGraphQLRequest for ready to chain for further initialization or execution.</returns>
64+
public static IFlurlGraphQLRequest UseGraphQLNewtonsoftJson(this IFlurlRequest request, Action<JsonSerializerSettings> configureJsonSerializerSettings)
65+
=> request.ToGraphQLRequest().UseGraphQLNewtonsoftJson(configureJsonSerializerSettings);
66+
67+
/// <summary>
68+
/// Configure the GraphQL Json Serializer settings using Newtonsoft.Json, but only for this GraphQL request; isolated from any other GraphQL Requests.
69+
/// </summary>
70+
/// <param name="graphqlRequest"></param>
71+
/// <param name="configureJsonSerializerSettings">Action method to configure the existing options as needed.</param>
72+
/// <returns>Returns an IFlurlGraphQLRequest for ready to chain for further initialization or execution.</returns>
73+
public static IFlurlGraphQLRequest UseGraphQLNewtonsoftJson(this IFlurlGraphQLRequest graphqlRequest, Action<JsonSerializerSettings> configureJsonSerializerSettings)
74+
{
75+
if (graphqlRequest is FlurlGraphQLRequest flurlGraphQLRequest)
76+
{
77+
var graphqlJsonSerializerOptions = FlurlGraphQLNewtonsoftJsonSerializer.CreateDefaultSerializerSettings();
78+
configureJsonSerializerSettings?.Invoke(graphqlJsonSerializerOptions);
79+
flurlGraphQLRequest.UseGraphQLNewtonsoftJson(graphqlJsonSerializerOptions);
5480
}
5581

5682
return graphqlRequest;
@@ -60,8 +86,6 @@ public static IFlurlGraphQLRequest UseGraphQLNewtonsoftJson(this IFlurlGraphQLRe
6086

6187
#region Json Parsing Extensions - JsonConvert Strategy
6288

63-
64-
6589
internal static JArray FlattenGraphQLEdgesJsonToArrayOfNodes(this JArray edgesJson)
6690
{
6791
var edgeNodesEnumerable = edgesJson
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
using Newtonsoft.Json.Serialization;
2+
using FlurlGraphQL;
3+
4+
public class FlurlGraphQLNewtonsoftJsonScreamingCaseNamingStrategy : NamingStrategy
5+
{
6+
protected override string ResolvePropertyName(string name) => name.ToScreamingCase();
7+
}

FlurlGraphQL.Newtonsoft/FlurlGraphQLNewtonsoftJsonSerializer.cs

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
using System;
2-
using System.IO;
1+
using System.IO;
32
using System.Threading.Tasks;
43
using Flurl.Http.Configuration;
54
using Flurl.Http.Newtonsoft;
@@ -8,6 +7,9 @@
87
using FlurlGraphQL.ValidationExtensions;
98
using Newtonsoft.Json.Linq;
109
using System.Collections.Generic;
10+
using System.Linq;
11+
using Newtonsoft.Json.Converters;
12+
using Newtonsoft.Json.Serialization;
1113

1214
namespace FlurlGraphQL.JsonProcessing
1315
{
@@ -20,18 +22,42 @@ public static IFlurlGraphQLJsonSerializer FromFlurlSerializer(ISerializer flurlS
2022
// and therefore must use dynamic instantiation via Reflection (leveraging brute force to access internal Options/Settings)
2123
// depending on if the System.Text.Json Serializer is used or if the Newtonsoft Json Serializer is being used.
2224
var currentJsonSettings = flurlSerializer.BruteForceGetFieldValue<JsonSerializerSettings>("_settings");
23-
24-
var graphqlJsonSettings = currentJsonSettings != null
25-
//Clone existing Options if available!
26-
? new JsonSerializerSettings(currentJsonSettings)
27-
//Default Options are used as fallback...
28-
: CreateDefaultSerializerSettings();
25+
26+
//Create the GraphQL Json Serializer options with critical default configuration being enforced!
27+
var graphqlJsonSettings = CreateDefaultSerializerSettings(currentJsonSettings);
2928

3029
return new FlurlGraphQLNewtonsoftJsonSerializer(graphqlJsonSettings);
3130
}
3231

33-
public static JsonSerializerSettings CreateDefaultSerializerSettings()
34-
=> JsonConvert.DefaultSettings?.Invoke() ?? new JsonSerializerSettings();
32+
public static JsonSerializerSettings CreateDefaultSerializerSettings(JsonSerializerSettings originalJsonSettings = null)
33+
{
34+
//Clone existing Options if available so we isolate our custom changes needed for GraphQL (e.g. custom String Enum converter, Case-insensitive, etc.)!
35+
var graphqlJsonSettings = originalJsonSettings != null
36+
//Clone existing Options if available!
37+
? new JsonSerializerSettings(originalJsonSettings)
38+
//Default Options are used as fallback...
39+
: JsonConvert.DefaultSettings?.Invoke() ?? new JsonSerializerSettings();
40+
41+
//For compatibility with FlurlGraphQL v1 behavior using Newtonsoft.Json it is case-insensitive by default but does not use Camel Case.
42+
//This is also helpful since GraphQL Json (and Json in general) use CamelCase and nearly always mismatch C# Naming Pascal Case standards of C# Class Models, etc...
43+
//NOTE: WE are operating on a copy of the original Json Settings so this does NOT mutate the core/original settings from Flurl or those specified for the GraphQL request, etc.
44+
graphqlJsonSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
45+
46+
var graphqlConfig = FlurlGraphQLConfig.DefaultConfig;
47+
48+
//For compatibility with FlurlGraphQL v1 behavior (using Newtonsoft.Json) we need to provide support for String to Enum conversion along with support for enum annotations
49+
// via [EnumMember(Value ="CustomName")] annotation (compatible with Newtonsoft.Json). In addition, we now also support [Description("CustomName")] annotation for
50+
// easier syntax that is arguably more intuitive to use.
51+
//NOTE: For performance we KNOW we need ot add this if original options were not provided (e.g. null)
52+
if (originalJsonSettings is null || !originalJsonSettings.Converters.OfType<StringEnumConverter>().Any())
53+
graphqlJsonSettings.Converters.Add(
54+
graphqlConfig.EnableAutomaticHandlingOfEnumsAsScreamingCaseStrings
55+
? new StringEnumConverter(namingStrategy: new FlurlGraphQLNewtonsoftJsonScreamingCaseNamingStrategy(), allowIntegerValues: true)
56+
: new StringEnumConverter()
57+
);
58+
59+
return graphqlJsonSettings;
60+
}
3561

3662
#region Base Flurl ISerializer implementation...
3763

FlurlGraphQL.Tests/FlurlGraphQLParsingTests.cs

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
using System.Collections.Generic;
22
using System.Linq;
3+
using System.Text.Json.Serialization;
4+
using Flurl.Http;
35
using Flurl.Http.Configuration;
46
using Flurl.Http.Newtonsoft;
57
using FlurlGraphQL.CustomExtensions;
68
using FlurlGraphQL.JsonProcessing;
79
using FlurlGraphQL.Tests.Models;
810
using Microsoft.VisualStudio.TestTools.UnitTesting;
11+
using Newtonsoft.Json;
12+
using Newtonsoft.Json.Linq;
913

1014
namespace FlurlGraphQL.Tests
1115
{
@@ -45,12 +49,85 @@ public void TestSimpleSystemTextJsonParsingOfErrors()
4549
}
4650

4751
[TestMethod]
48-
public void TestSimpleSystemTextJsonParsingOfPreFlattenedJsonWithStringEnumWithAnnotationJsonConverter()
52+
[TestDataExecuteWithAllFlurlSerializerRequests]
53+
public void TestSimpleSystemTextJsonParsingOfPreFlattenedJsonWithStringEnumWithEnumMemberAnnotationConversion(IFlurlRequest flurlRequest)
4954
{
55+
var graphqlApiRequest = flurlRequest.ToGraphQLRequest();
56+
//Be sure that we ignore Null properties so that the serialization Comparison at the end works as expected!
57+
switch (graphqlApiRequest.GraphQLJsonSerializer)
58+
{
59+
case FlurlGraphQLSystemTextJsonSerializer _:
60+
graphqlApiRequest.UseGraphQLSystemTextJson(o =>
61+
{
62+
o.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
63+
});
64+
break;
65+
case FlurlGraphQLNewtonsoftJsonSerializer _:
66+
graphqlApiRequest.UseGraphQLNewtonsoftJson(s =>
67+
{
68+
s.NullValueHandling = NullValueHandling.Ignore;
69+
});
70+
break;
71+
}
72+
5073
//NOTE: We leverage Internal Methods and Classes here to get lower level access for Unit Testing and Quicker Debugging...
51-
var graphqlSerializer = FlurlGraphQLSystemTextJsonSerializer.FromFlurlSerializer(new DefaultJsonSerializer());
74+
var graphqlSerializer = graphqlApiRequest.GraphQLJsonSerializer;
5275

53-
var characterResults = graphqlSerializer.Deserialize<List<StarWarsCharacterWithEnum>>(NestedJsonStructureFlattenedWithForceEnum);
76+
var characterResults = graphqlSerializer.Deserialize<List<StarWarsCharacterWithEnumMember>>(NestedJsonStructureFlattenedWithForceEnum);
77+
78+
Assert.IsNotNull(characterResults);
79+
Assert.AreEqual(2, characterResults.Count);
80+
Assert.AreEqual(TheForceWithEnumMembers.LightSideForGood, characterResults.First(c => c.Name.StartsWith("Luke")).TheForce);
81+
Assert.AreEqual(TheForceWithEnumMembers.DarkSideForEvil, characterResults.First(c => c.Name.StartsWith("Darth")).TheForce);
82+
83+
foreach (var result in characterResults)
84+
{
85+
TestContext.WriteLine($"Character [{result.PersonalIdentifier}] [{result.Name}] [{result.Height}]");
86+
87+
foreach (var friend in result.Friends)
88+
{
89+
Assert.IsNotNull(friend);
90+
Assert.AreEqual(TheForceWithEnumMembers.NoneAtAll, friend.TheForce);
91+
TestContext.WriteLine($" Friend [{friend.PersonalIdentifier}] [{friend.Name}] [{friend.Height}]");
92+
}
93+
}
94+
95+
//TEST Re-serialization should Match the original Json!
96+
var json = graphqlSerializer.Serialize(characterResults);
97+
bool isEqual = JToken.DeepEquals(
98+
JArray.Parse(NestedJsonStructureFlattenedWithForceEnum), //The Test Data
99+
JArray.Parse(json) //The Round Trip Serialized data
100+
);
101+
102+
Assert.IsTrue(isEqual, "The Test Json and the Round Trip Serialized Json do not match!");
103+
}
104+
105+
106+
[TestMethod]
107+
[TestDataExecuteWithAllFlurlSerializerRequests]
108+
public void TestSimpleSystemTextJsonParsingOfPreFlattenedJsonWithStringEnumDirectConversion(IFlurlRequest flurlRequest)
109+
{
110+
var graphqlApiRequest = flurlRequest.ToGraphQLRequest();
111+
//Be sure that we ignore Null properties so that the serialization Comparison at the end works as expected!
112+
switch (graphqlApiRequest.GraphQLJsonSerializer)
113+
{
114+
case FlurlGraphQLSystemTextJsonSerializer _:
115+
graphqlApiRequest.UseGraphQLSystemTextJson(o =>
116+
{
117+
o.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
118+
});
119+
break;
120+
case FlurlGraphQLNewtonsoftJsonSerializer _:
121+
graphqlApiRequest.UseGraphQLNewtonsoftJson(s =>
122+
{
123+
s.NullValueHandling = NullValueHandling.Ignore;
124+
});
125+
break;
126+
}
127+
128+
var graphqlSerializer = graphqlApiRequest.GraphQLJsonSerializer;
129+
130+
var characterResults = graphqlSerializer.Deserialize<List<StarWarsCharacterWithSimpleEnum>>(NestedJsonStructureFlattenedWithForceEnum);
54131

55132
Assert.IsNotNull(characterResults);
56133
Assert.AreEqual(2, characterResults.Count);
@@ -68,6 +145,15 @@ public void TestSimpleSystemTextJsonParsingOfPreFlattenedJsonWithStringEnumWithA
68145
TestContext.WriteLine($" Friend [{friend.PersonalIdentifier}] [{friend.Name}] [{friend.Height}]");
69146
}
70147
}
148+
149+
//TEST Re-serialization should Match the original Json!
150+
var json = graphqlSerializer.Serialize(characterResults);
151+
bool isEqual = JToken.DeepEquals(
152+
JArray.Parse(NestedJsonStructureFlattenedWithForceEnum), //The Test Data
153+
JArray.Parse(json) //The Round Trip Serialized data
154+
);
155+
156+
Assert.IsTrue(isEqual, "The Test Json and the Round Trip Serialized Json do not match!");
71157
}
72158

73159
[TestMethod]

FlurlGraphQL.Tests/FlurlGraphQLQueryingPersistedQueryTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public async Task TestPersistedPostFailsForInvalidOverrideFieldNameAsync(IFlurlR
6262
.SetPersistedQueryPayloadFieldName(RELAY_PERSISTED_QUERY_KEY)
6363
.SetGraphQLVariables(new { first = 2, friendsCount = 1 });
6464

65-
Assert.AreEqual(RELAY_PERSISTED_QUERY_KEY, request.PersistedQueryPayloadFieldName);
65+
Assert.AreEqual(RELAY_PERSISTED_QUERY_KEY, request.GraphQLConfig.PersistedQueryPayloadFieldName);
6666

6767
var results = await request.PostGraphQLQueryAsync()
6868
.ReceiveGraphQLQueryResults<StarWarsCharacter>()
@@ -98,7 +98,7 @@ public async Task TestPersistedPostFailsForInvalidGloballyConfiguredFieldNameAsy
9898
.WithGraphQLPersistedQuery("AllCharactersWithFriendsPaginated-v1")
9999
.SetGraphQLVariables(new { first = 2, friendsCount = 1 });
100100

101-
Assert.AreEqual(RELAY_PERSISTED_QUERY_KEY, request.PersistedQueryPayloadFieldName);
101+
Assert.AreEqual(RELAY_PERSISTED_QUERY_KEY, request.GraphQLConfig.PersistedQueryPayloadFieldName);
102102

103103
var results = await request.PostGraphQLQueryAsync()
104104
.ReceiveGraphQLQueryResults<StarWarsCharacter>()

FlurlGraphQL.Tests/FlurlGraphQLQueryingSimplePostTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ public async Task TestSinglePostQueryWithOnlyNestedPaginatedResultsAsync(IFlurlR
205205
}
206206
}
207207
")
208-
.SetGraphQLVariables(new { ids = new[] { 1000, 2001 }, friendsCount = friendCountParam })
208+
.SetGraphQLVariables(new { ids = idArrayParam, friendsCount = friendCountParam })
209209
.PostGraphQLQueryAsync()
210210
.ReceiveGraphQLQueryResults<StarWarsCharacter>()
211211
.ConfigureAwait(false);

0 commit comments

Comments
 (0)