Skip to content

Commit f7659c8

Browse files
authored
Deserialization updates (#569)
1 parent 916ddec commit f7659c8

File tree

10 files changed

+288
-11
lines changed

10 files changed

+288
-11
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ jobs:
6868
if: ${{ startsWith(matrix.os, 'ubuntu') }}
6969
uses: codecov/[email protected]
7070
with:
71-
files: .coverage/GraphQL.Server.Authorization.AspNetCore.Tests/coverage.net5.opencover.xml,.coverage/GraphQL.Server.Samples.Server.Tests/coverage.netcoreapp3.1.opencover.xml,.coverage/GraphQL.Server.Transports.Subscriptions.Abstractions.Tests/coverage.net5.opencover.xml,.coverage/GraphQL.Server.Transports.Subscriptions.WebSockets.Tests/coverage.net5.opencover.xml
71+
files: .coverage/GraphQL.Server.Authorization.AspNetCore.Tests/coverage.net5.opencover.xml,.coverage/GraphQL.Server.Samples.Server.Tests/coverage.netcoreapp3.1.opencover.xml,.coverage/GraphQL.Server.Transports.Subscriptions.Abstractions.Tests/coverage.net5.opencover.xml,.coverage/GraphQL.Server.Transports.Subscriptions.WebSockets.Tests/coverage.net5.opencover.xml,.coverage/GraphQL.Server.Transports.AspNetCore.Tests/coverage.net5.opencover.xml
7272

7373
buildcheck:
7474
needs:

GraphQL.Server.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiApprovalTests", "tests\A
7979
EndProject
8080
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "All", "src\All\All.csproj", "{B39BC064-B96A-442D-89A0-79B750F88ADD}"
8181
EndProject
82+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Transports.AspNetCore.Tests", "tests\Transports.AspNetCore.Tests\Transports.AspNetCore.Tests.csproj", "{1857A25B-C4A3-4332-9077-3F2516AC6059}"
83+
EndProject
8284
Global
8385
GlobalSection(SolutionConfigurationPlatforms) = preSolution
8486
Debug|Any CPU = Debug|Any CPU
@@ -165,6 +167,10 @@ Global
165167
{B39BC064-B96A-442D-89A0-79B750F88ADD}.Debug|Any CPU.Build.0 = Debug|Any CPU
166168
{B39BC064-B96A-442D-89A0-79B750F88ADD}.Release|Any CPU.ActiveCfg = Release|Any CPU
167169
{B39BC064-B96A-442D-89A0-79B750F88ADD}.Release|Any CPU.Build.0 = Release|Any CPU
170+
{1857A25B-C4A3-4332-9077-3F2516AC6059}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
171+
{1857A25B-C4A3-4332-9077-3F2516AC6059}.Debug|Any CPU.Build.0 = Debug|Any CPU
172+
{1857A25B-C4A3-4332-9077-3F2516AC6059}.Release|Any CPU.ActiveCfg = Release|Any CPU
173+
{1857A25B-C4A3-4332-9077-3F2516AC6059}.Release|Any CPU.Build.0 = Release|Any CPU
168174
EndGlobalSection
169175
GlobalSection(SolutionProperties) = preSolution
170176
HideSolutionNode = FALSE
@@ -185,6 +191,7 @@ Global
185191
{C1FCE2C8-F6EF-48DA-ADD8-9159516D03DA} = {BBD07745-C962-4D2D-B302-6DA1BCC2FF43}
186192
{17945A3F-4C80-43B3-BBD7-6FE0EA107799} = {592C7A03-4F0C-40C2-86F9-C7A4E03504A7}
187193
{9F1A96BC-FF87-427F-8284-5A0670CA596C} = {BBD07745-C962-4D2D-B302-6DA1BCC2FF43}
194+
{1857A25B-C4A3-4332-9077-3F2516AC6059} = {BBD07745-C962-4D2D-B302-6DA1BCC2FF43}
188195
EndGlobalSection
189196
GlobalSection(ExtensibilityGlobals) = postSolution
190197
SolutionGuid = {3FC7FA59-E938-453C-8C4A-9D5635A9489A}

src/Transports.AspNetCore.NewtonsoftJson/GraphQLRequestDeserializer.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,15 @@ public class GraphQLRequestDeserializer : IGraphQLRequestDeserializer
1818

1919
public GraphQLRequestDeserializer(Action<JsonSerializerSettings> configure)
2020
{
21-
var settings = new JsonSerializerSettings();
21+
var settings = new JsonSerializerSettings
22+
{
23+
DateFormatHandling = DateFormatHandling.IsoDateFormat,
24+
DateParseHandling = DateParseHandling.None,
25+
Converters =
26+
{
27+
new InputsConverter()
28+
},
29+
};
2230
configure?.Invoke(settings);
2331
_serializer = JsonSerializer.Create(settings); // it's thread safe https://stackoverflow.com/questions/36186276/is-the-json-net-jsonserializer-threadsafe
2432
}
@@ -76,8 +84,8 @@ private static GraphQLRequest ToGraphQLRequest(InternalGraphQLRequest internalGr
7684
{
7785
OperationName = internalGraphQLRequest.OperationName,
7886
Query = internalGraphQLRequest.Query,
79-
Inputs = internalGraphQLRequest.Variables?.ToInputs(), // must return null if not provided, not an empty dictionary
80-
Extensions = internalGraphQLRequest.Extensions?.ToInputs(), // must return null if not provided, not an empty dictionary
87+
Inputs = internalGraphQLRequest.Variables, // must return null if not provided, not an empty Inputs
88+
Extensions = internalGraphQLRequest.Extensions, // must return null if not provided, not an empty Inputs
8189
};
8290
}
8391
}

src/Transports.AspNetCore.NewtonsoftJson/InternalGraphQLRequest.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using Newtonsoft.Json;
2-
using Newtonsoft.Json.Linq;
32

43
namespace GraphQL.Server.Transports.AspNetCore.NewtonsoftJson
54
{
@@ -15,9 +14,9 @@ internal sealed class InternalGraphQLRequest
1514
public string Query { get; set; }
1615

1716
[JsonProperty(GraphQLRequest.VARIABLES_KEY)]
18-
public JObject Variables { get; set; }
17+
public Inputs Variables { get; set; }
1918

2019
[JsonProperty(GraphQLRequest.EXTENSIONS_KEY)]
21-
public JObject Extensions { get; set; }
20+
public Inputs Extensions { get; set; }
2221
}
2322
}

src/Transports.AspNetCore.SystemTextJson/GraphQLRequestDeserializer.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public class GraphQLRequestDeserializer : IGraphQLRequestDeserializer
2323

2424
public GraphQLRequestDeserializer(Action<JsonSerializerOptions> configure)
2525
{
26-
// Add converter that deserializes Variables property
26+
// Add converter that deserializes Variables and Extensions properties
2727
_serializerOptions.Converters.Add(new InputsConverter());
2828

2929
configure?.Invoke(_serializerOptions);
@@ -127,8 +127,8 @@ private static GraphQLRequest ToGraphQLRequest(InternalGraphQLRequest internalGr
127127
{
128128
OperationName = internalGraphQLRequest.OperationName,
129129
Query = internalGraphQLRequest.Query,
130-
Inputs = internalGraphQLRequest.Variables, // must return null if not provided, not an empty dictionary
131-
Extensions = internalGraphQLRequest.Extensions, // must return null if not provided, not an empty dictionary
130+
Inputs = internalGraphQLRequest.Variables, // must return null if not provided, not an empty Inputs
131+
Extensions = internalGraphQLRequest.Extensions, // must return null if not provided, not an empty Inputs
132132
};
133133
}
134134
}

src/Transports.Subscriptions.Abstractions/Transports.Subscriptions.Abstractions.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFramework>netstandard2.0</TargetFramework>
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
using System.Collections.Generic;
2+
using System.Numerics;
3+
using System.Text;
4+
using System.Threading.Tasks;
5+
using Shouldly;
6+
using Xunit;
7+
8+
namespace GraphQL.Server.Transports.AspNetCore.Tests
9+
{
10+
public partial class NewtonsoftJsonTests
11+
{
12+
[Fact]
13+
public async Task Decodes_Request()
14+
{
15+
var request = @"{""query"":""abc"",""operationName"":""def"",""variables"":{""a"":""b"",""c"":2},""extensions"":{""d"":""e"",""f"":3}}";
16+
var ret = await Deserialize(request);
17+
ret.IsSuccessful.ShouldBeTrue();
18+
ret.Single.Query.ShouldBe("abc");
19+
ret.Single.OperationName.ShouldBe("def");
20+
ret.Single.Inputs["a"].ShouldBe("b");
21+
ret.Single.Inputs["c"].ShouldBe(2);
22+
ret.Single.Extensions["d"].ShouldBe("e");
23+
ret.Single.Extensions["f"].ShouldBe(3);
24+
}
25+
26+
[Fact]
27+
public async Task Decodes_Empty_Request()
28+
{
29+
var request = @"{}";
30+
var ret = await Deserialize(request);
31+
ret.IsSuccessful.ShouldBeTrue();
32+
ret.Single.Query.ShouldBeNull();
33+
ret.Single.OperationName.ShouldBeNull();
34+
ret.Single.Inputs.ShouldBeNull();
35+
ret.Single.Extensions.ShouldBeNull();
36+
}
37+
38+
[Fact]
39+
public async Task Decodes_BigInteger()
40+
{
41+
var request = @"{""variables"":{""a"":1234567890123456789012345678901234567890}}";
42+
var ret = await Deserialize(request);
43+
var bi = BigInteger.Parse("1234567890123456789012345678901234567890");
44+
ret.Single.Inputs["a"].ShouldBeOfType<BigInteger>().ShouldBe(bi);
45+
}
46+
47+
[Fact]
48+
public async Task Dates_Should_Parse_As_Text()
49+
{
50+
var ret = await Deserialize(@"{""variables"":{""date"":""2015-12-22T10:10:10+03:00""}}");
51+
ret.Single.Inputs["date"].ShouldBeOfType<string>().ShouldBe("2015-12-22T10:10:10+03:00");
52+
}
53+
54+
[Fact]
55+
public async Task Extensions_Null_When_Not_Provided()
56+
{
57+
var ret = await Deserialize(@"{""variables"":{""date"":""2015-12-22T10:10:10+03:00""}}");
58+
ret.Single.Extensions.ShouldBeNull();
59+
}
60+
61+
[Fact]
62+
public async Task Name_Matching_Not_Case_Sensitive()
63+
{
64+
var ret = await Deserialize(@"{""VARIABLES"":{""date"":""2015-12-22T10:10:10+03:00""}}");
65+
ret.Single.Inputs["date"].ShouldBeOfType<string>().ShouldBe("2015-12-22T10:10:10+03:00");
66+
}
67+
68+
[Fact]
69+
public async Task Decodes_Multiple_Queries()
70+
{
71+
var ret = await Deserialize(@"[{""query"":""abc""},{""query"":""def""}]");
72+
ret.Batch.Length.ShouldBe(2);
73+
ret.Batch[0].Query.ShouldBe("abc");
74+
ret.Batch[1].Query.ShouldBe("def");
75+
}
76+
77+
[Fact]
78+
public async Task Decodes_Nested_Dictionaries()
79+
{
80+
var ret = await Deserialize(@"{""variables"":{""a"":{""b"":""c""}},""extensions"":{""d"":{""e"":""f""}}}");
81+
ret.Single.Inputs["a"].ShouldBeOfType<Dictionary<string, object>>()["b"].ShouldBe("c");
82+
ret.Single.Extensions["d"].ShouldBeOfType<Dictionary<string, object>>()["e"].ShouldBe("f");
83+
}
84+
85+
[Fact]
86+
public async Task Decodes_Nested_Arrays()
87+
{
88+
var ret = await Deserialize(@"{""variables"":{""a"":[""b"",""c""]},""extensions"":{""d"":[""e"",""f""]}}");
89+
ret.Single.Inputs["a"].ShouldBeOfType<List<object>>().ShouldBe(new[] { "b", "c" });
90+
ret.Single.Extensions["d"].ShouldBeOfType<List<object>>().ShouldBe(new[] { "e", "f" });
91+
}
92+
93+
private async Task<GraphQLRequestDeserializationResult> Deserialize(string jsonText)
94+
{
95+
var jsonStream = new System.IO.MemoryStream(Encoding.UTF8.GetBytes(jsonText));
96+
var httpRequest = new TestHttpRequest
97+
{
98+
Body = jsonStream
99+
};
100+
var deserializer = new NewtonsoftJson.GraphQLRequestDeserializer(_ => { });
101+
return await deserializer.DeserializeFromJsonBodyAsync(httpRequest);
102+
}
103+
}
104+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
using System.Collections.Generic;
2+
using System.Numerics;
3+
using System.Text;
4+
using System.Threading.Tasks;
5+
using Shouldly;
6+
using Xunit;
7+
8+
namespace GraphQL.Server.Transports.AspNetCore.Tests
9+
{
10+
public partial class SystemTextJsonTests
11+
{
12+
[Fact]
13+
public async Task Decodes_Request()
14+
{
15+
var request = @"{""query"":""abc"",""operationName"":""def"",""variables"":{""a"":""b"",""c"":2},""extensions"":{""d"":""e"",""f"":3}}";
16+
var ret = await Deserialize(request);
17+
ret.IsSuccessful.ShouldBeTrue();
18+
ret.Single.Query.ShouldBe("abc");
19+
ret.Single.OperationName.ShouldBe("def");
20+
ret.Single.Inputs["a"].ShouldBe("b");
21+
ret.Single.Inputs["c"].ShouldBe(2);
22+
ret.Single.Extensions["d"].ShouldBe("e");
23+
ret.Single.Extensions["f"].ShouldBe(3);
24+
}
25+
26+
[Fact]
27+
public async Task Decodes_Empty_Request()
28+
{
29+
var request = @"{}";
30+
var ret = await Deserialize(request);
31+
ret.IsSuccessful.ShouldBeTrue();
32+
ret.Single.Query.ShouldBeNull();
33+
ret.Single.OperationName.ShouldBeNull();
34+
ret.Single.Inputs.ShouldBeNull();
35+
ret.Single.Extensions.ShouldBeNull();
36+
}
37+
38+
[Fact]
39+
public async Task Decodes_BigInteger()
40+
{
41+
var request = @"{""variables"":{""a"":1234567890123456789012345678901234567890}}";
42+
var ret = await Deserialize(request);
43+
var bi = BigInteger.Parse("1234567890123456789012345678901234567890");
44+
ret.Single.Inputs["a"].ShouldBeOfType<BigInteger>().ShouldBe(bi);
45+
}
46+
47+
[Fact]
48+
public async Task Dates_Should_Parse_As_Text()
49+
{
50+
var ret = await Deserialize(@"{""variables"":{""date"":""2015-12-22T10:10:10+03:00""}}");
51+
ret.Single.Inputs["date"].ShouldBeOfType<string>().ShouldBe("2015-12-22T10:10:10+03:00");
52+
}
53+
54+
[Fact]
55+
public async Task Extensions_Null_When_Not_Provided()
56+
{
57+
var ret = await Deserialize(@"{""variables"":{""date"":""2015-12-22T10:10:10+03:00""}}");
58+
ret.Single.Extensions.ShouldBeNull();
59+
}
60+
61+
[Fact]
62+
public async Task Name_Matching_Is_Case_Sensitive()
63+
{
64+
var ret = await Deserialize(@"{""VARIABLES"":{""date"":""2015-12-22T10:10:10+03:00""}}");
65+
ret.Single.Inputs.ShouldBeNull();
66+
}
67+
68+
[Fact]
69+
public async Task Decodes_Multiple_Queries()
70+
{
71+
var ret = await Deserialize(@"[{""query"":""abc""},{""query"":""def""}]");
72+
ret.Batch.Length.ShouldBe(2);
73+
ret.Batch[0].Query.ShouldBe("abc");
74+
ret.Batch[1].Query.ShouldBe("def");
75+
}
76+
77+
[Fact]
78+
public async Task Decodes_Nested_Dictionaries()
79+
{
80+
var ret = await Deserialize(@"{""variables"":{""a"":{""b"":""c""}},""extensions"":{""d"":{""e"":""f""}}}");
81+
ret.Single.Inputs["a"].ShouldBeOfType<Dictionary<string, object>>()["b"].ShouldBe("c");
82+
ret.Single.Extensions["d"].ShouldBeOfType<Dictionary<string, object>>()["e"].ShouldBe("f");
83+
}
84+
85+
[Fact]
86+
public async Task Decodes_Nested_Arrays()
87+
{
88+
var ret = await Deserialize(@"{""variables"":{""a"":[""b"",""c""]},""extensions"":{""d"":[""e"",""f""]}}");
89+
ret.Single.Inputs["a"].ShouldBeOfType<List<object>>().ShouldBe(new[] { "b", "c" });
90+
ret.Single.Extensions["d"].ShouldBeOfType<List<object>>().ShouldBe(new[] { "e", "f" });
91+
}
92+
93+
private async Task<GraphQLRequestDeserializationResult> Deserialize(string jsonText)
94+
{
95+
var jsonStream = new System.IO.MemoryStream(Encoding.UTF8.GetBytes(jsonText));
96+
var httpRequest = new TestHttpRequest
97+
{
98+
Body = jsonStream,
99+
};
100+
var deserializer = new SystemTextJson.GraphQLRequestDeserializer(_ => { });
101+
return await deserializer.DeserializeFromJsonBodyAsync(httpRequest);
102+
}
103+
}
104+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System;
2+
using System.IO.Pipelines;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Http;
6+
7+
namespace GraphQL.Server.Transports.AspNetCore.Tests
8+
{
9+
public class TestHttpRequest : HttpRequest
10+
{
11+
public override HttpContext HttpContext => throw new NotImplementedException();
12+
13+
public override string Method { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
14+
public override string Scheme { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
15+
public override bool IsHttps { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
16+
public override HostString Host { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
17+
public override PathString PathBase { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
18+
public override PathString Path { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
19+
public override QueryString QueryString { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
20+
public override IQueryCollection Query { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
21+
public override string Protocol { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
22+
23+
public override IHeaderDictionary Headers => throw new NotImplementedException();
24+
25+
public override IRequestCookieCollection Cookies { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
26+
public override long? ContentLength { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
27+
public override string ContentType { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
28+
public override System.IO.Stream Body { get; set; }
29+
public override PipeReader BodyReader => PipeReader.Create(Body);
30+
31+
public override bool HasFormContentType => throw new NotImplementedException();
32+
33+
public override IFormCollection Form { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
34+
35+
public override Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException();
36+
}
37+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<Import Project="../../Tests.props" />
3+
4+
<PropertyGroup>
5+
<TargetFrameworks>net5;netcoreapp3.1</TargetFrameworks>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<FrameworkReference Include="Microsoft.AspNetCore.App" />
10+
<PackageReference Include="Castle.Core" Version="4.4.1" />
11+
</ItemGroup>
12+
13+
<ItemGroup>
14+
<ProjectReference Include="..\..\src\Transports.AspNetCore.NewtonsoftJson\Transports.AspNetCore.NewtonsoftJson.csproj" />
15+
<ProjectReference Include="..\..\src\Transports.AspNetCore.SystemTextJson\Transports.AspNetCore.SystemTextJson.csproj" />
16+
</ItemGroup>
17+
18+
</Project>

0 commit comments

Comments
 (0)