Skip to content

Commit 5ca67c4

Browse files
committed
fix ProblemDetail Deserialize, add a QueryString IEnumerable overload
1 parent 15a527b commit 5ca67c4

File tree

7 files changed

+184
-116
lines changed

7 files changed

+184
-116
lines changed

src/FluentRest/ProblemDetails.cs

+36-11
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
using System.Text.Json.Serialization;
22

3+
#nullable enable
4+
35
namespace FluentRest;
46

57
/// <summary>
68
/// A machine-readable format for specifying errors in HTTP API responses based on https://tools.ietf.org/html/rfc7807.
79
/// </summary>
8-
[JsonConverter(typeof(ProblemDetailsConverter))]
910
public class ProblemDetails
1011
{
1112
/// <summary>
@@ -14,43 +15,67 @@ public class ProblemDetails
1415
public const string ContentType = "application/problem+json";
1516

1617
/// <summary>
17-
/// A URI reference that identifies the problem type.
18+
/// A URI reference [RFC3986] that identifies the problem type. This specification encourages that, when
19+
/// dereferenced, it provide human-readable documentation for the problem type
20+
/// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be
21+
/// "about:blank".
1822
/// </summary>
1923
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
24+
[JsonPropertyOrder(-5)]
2025
[JsonPropertyName("type")]
21-
public string Type { get; set; }
26+
public string? Type { get; set; }
2227

2328
/// <summary>
24-
/// A short, human-readable summary of the problem type.
29+
/// A short, human-readable summary of the problem type. It SHOULD NOT change from occurrence to occurrence
30+
/// of the problem, except for purposes of localization(e.g., using proactive content negotiation;
31+
/// see[RFC7231], Section 3.4).
2532
/// </summary>
2633
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
34+
[JsonPropertyOrder(-4)]
2735
[JsonPropertyName("title")]
28-
public string Title { get; set; }
36+
public string? Title { get; set; }
2937

3038
/// <summary>
31-
/// The HTTP status code generated by the origin server for this occurrence of the problem.
39+
/// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.
3240
/// </summary>
3341
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
42+
[JsonPropertyOrder(-3)]
3443
[JsonPropertyName("status")]
3544
public int? Status { get; set; }
3645

3746
/// <summary>
3847
/// A human-readable explanation specific to this occurrence of the problem.
3948
/// </summary>
4049
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
50+
[JsonPropertyOrder(-2)]
4151
[JsonPropertyName("detail")]
42-
public string Detail { get; set; }
52+
public string? Detail { get; set; }
4353

4454
/// <summary>
45-
/// A URI reference that identifies the specific occurrence of the problem.
55+
/// A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced.
4656
/// </summary>
4757
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
58+
[JsonPropertyOrder(-1)]
4859
[JsonPropertyName("instance")]
49-
public string Instance { get; set; }
60+
public string? Instance { get; set; }
61+
62+
/// <summary>
63+
/// Gets the validation errors associated with this instance of problem details
64+
/// </summary>
65+
[JsonPropertyName("errors")]
66+
public IDictionary<string, string[]> Errors { get; set; } = new Dictionary<string, string[]>(StringComparer.Ordinal);
5067

5168
/// <summary>
52-
/// Problem type definitions MAY extend the problem details object with additional members.
69+
/// Gets the <see cref="IDictionary{TKey, TValue}"/> for extension members.
70+
/// <para>
71+
/// Problem type definitions MAY extend the problem details object with additional members. Extension members appear in the same namespace as
72+
/// other members of a problem type.
73+
/// </para>
5374
/// </summary>
75+
/// <remarks>
76+
/// The round-tripping behavior for <see cref="Extensions"/> is determined by the implementation of the Input \ Output formatters.
77+
/// In particular, complex types or collection types may not round-trip to the original type when using the built-in JSON or XML formatters.
78+
/// </remarks>
5479
[JsonExtensionData]
55-
public IDictionary<string, object> Extensions { get; } = new Dictionary<string, object>(StringComparer.Ordinal);
80+
public IDictionary<string, object?> Extensions { get; set; } = new Dictionary<string, object?>(StringComparer.Ordinal);
5681
}

src/FluentRest/ProblemDetailsConverter.cs

-105
This file was deleted.

src/FluentRest/QueryBuilder.cs

+28
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,34 @@ public TBuilder QueryString<TValue>(string name, TValue value)
374374
return QueryString(name, v);
375375
}
376376

377+
/// <summary>
378+
/// Appends the specified <paramref name="name" /> and <paramref name="values" /> to the request Uri.
379+
/// </summary>
380+
/// <typeparam name="TValue">The type of the value.</typeparam>
381+
/// <param name="name">The query parameter name.</param>
382+
/// <param name="values">The query parameter values.</param>
383+
/// <returns>
384+
/// A fluent request builder.
385+
/// </returns>
386+
/// <exception cref="System.ArgumentNullException"><paramref name="name" /> is <see langword="null" />.</exception>
387+
public TBuilder QueryString<TValue>(string name, IEnumerable<TValue> values)
388+
{
389+
if (name == null)
390+
throw new ArgumentNullException(nameof(name));
391+
392+
if (values == null)
393+
return this as TBuilder;
394+
395+
foreach (var value in values)
396+
{
397+
var v = value != null ? value.ToString() : string.Empty;
398+
QueryString(name, v);
399+
}
400+
401+
return this as TBuilder;
402+
}
403+
404+
377405
/// <summary>
378406
/// Appends the specified <paramref name="name"/> and <paramref name="value"/> to the request Uri if the specified <paramref name="condition"/> is true.
379407
/// </summary>

src/FluentRest/UrlBuilder.cs

+25
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,31 @@ public UrlBuilder AppendQuery<TValue>(string name, TValue value)
454454
return AppendQuery(name, v);
455455
}
456456

457+
/// <summary>
458+
/// Appends the query string name and values to the current url.
459+
/// </summary>
460+
/// <typeparam name="TValue">The type of the value.</typeparam>
461+
/// <param name="name">The query string name.</param>
462+
/// <param name="values">The query string values.</param>
463+
/// <returns></returns>
464+
/// <exception cref="ArgumentNullException">name is <c>null</c></exception>
465+
public UrlBuilder AppendQuery<TValue>(string name, IEnumerable<TValue> values)
466+
{
467+
if (name == null)
468+
throw new ArgumentNullException(nameof(name));
469+
470+
if (values == null)
471+
return this;
472+
473+
foreach (var value in values)
474+
{
475+
var v = value != null ? value.ToString() : string.Empty;
476+
AppendQuery(name, v);
477+
}
478+
479+
return this;
480+
}
481+
457482
/// <summary>
458483
/// Conditionally appends the query string name and value to the current url if the specified <paramref name="condition" /> is <c>true</c>.
459484
/// </summary>

test/FluentRest.Tests/FluentRest.Tests.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<PropertyGroup>
33
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
44
<IsPackable>false</IsPackable>
5+
<LangVersion>Latest</LangVersion>
56
</PropertyGroup>
67
<ItemGroup>
78
<ProjectReference Include="..\..\src\FluentRest\FluentRest.csproj" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
4+
using FluentAssertions;
5+
6+
using Xunit;
7+
8+
namespace FluentRest.Tests;
9+
10+
public class ProblemDetailsTests
11+
{
12+
[Fact]
13+
public void DeserializeBadRequestValidation()
14+
{
15+
var options = CreateOptions();
16+
17+
var json = @"{
18+
""type"": ""https://tools.ietf.org/html/rfc9110#section-15.5.1"",
19+
""title"": ""One or more validation errors occurred."",
20+
""status"": 400,
21+
""errors"": {
22+
""caseManagerIds"": [
23+
""The value 'System.Collections.Generic.List`1[System.Int32]' is not valid.""
24+
]
25+
},
26+
""traceId"": ""00-894a84e8e9621fb8fcabcb2911e6d51c-5a4777d830f30cff-00""
27+
}";
28+
var problemDetails = JsonSerializer.Deserialize<ProblemDetails>(json, options);
29+
problemDetails.Should().NotBeNull();
30+
problemDetails.Title.Should().Be("One or more validation errors occurred.");
31+
problemDetails.Status.Should().Be(400);
32+
}
33+
34+
35+
36+
[Fact]
37+
public void DeserializeServerError()
38+
{
39+
var options = CreateOptions();
40+
41+
var json = @"{
42+
""type"": ""https://tools.ietf.org/html/rfc9110#section-15.5.1"",
43+
""title"": ""One or more errors occurred."",
44+
""status"": 500,
45+
""traceId"": ""00-894a84e8e9621fb8fcabcb2911e6d51c-5a4777d830f30cff-00""
46+
}";
47+
var problemDetails = JsonSerializer.Deserialize<ProblemDetails>(json, options);
48+
problemDetails.Should().NotBeNull();
49+
problemDetails.Title.Should().Be("One or more errors occurred.");
50+
problemDetails.Status.Should().Be(500);
51+
}
52+
53+
[Fact]
54+
public void DeserializeServerErrorExtra()
55+
{
56+
var options = CreateOptions();
57+
58+
var json = @"{
59+
""type"": ""https://tools.ietf.org/html/rfc9110#section-15.5.1"",
60+
""title"": ""One or more errors occurred."",
61+
""status"": 500,
62+
""exception"": ""this is an exception"",
63+
""traceId"": ""00-894a84e8e9621fb8fcabcb2911e6d51c-5a4777d830f30cff-00""
64+
}";
65+
var problemDetails = JsonSerializer.Deserialize<ProblemDetails>(json, options);
66+
problemDetails.Should().NotBeNull();
67+
problemDetails.Title.Should().Be("One or more errors occurred.");
68+
problemDetails.Status.Should().Be(500);
69+
}
70+
71+
private static JsonSerializerOptions CreateOptions()
72+
{
73+
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
74+
options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
75+
options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault;
76+
options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
77+
options.TypeInfoResolverChain.Add(ProblemDetailsJsonContext.Default);
78+
return options;
79+
}
80+
}

test/FluentRest.Tests/QueryBuilderTest.cs

+14
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,20 @@ public void QueryStringMultipleValue()
3838
Assert.Equal("http://test.com/?Test=Test1&Test=Test2", uri.ToString());
3939
}
4040

41+
[Fact]
42+
public void QueryStringMultipleList()
43+
{
44+
var request = new HttpRequestMessage();
45+
var builder = new QueryBuilder(request);
46+
47+
builder.BaseUri("http://test.com/");
48+
builder.QueryString("Test", ["Test1", "Test2"]);
49+
50+
var uri = request.GetUrlBuilder();
51+
52+
Assert.Equal("http://test.com/?Test=Test1&Test=Test2", uri.ToString());
53+
}
54+
4155
[Fact]
4256
public void HeaderSingleValue()
4357
{

0 commit comments

Comments
 (0)