Skip to content

Commit e34709d

Browse files
author
Bart Koelman
authored
Adds option to emit jsonapi version in response documents (#992)
1 parent 7008d2e commit e34709d

File tree

9 files changed

+192
-2
lines changed

9 files changed

+192
-2
lines changed

src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ internal NamingStrategy SerializerNamingStrategy
3434
/// </summary>
3535
AttrCapabilities DefaultAttrCapabilities { get; }
3636

37+
/// <summary>
38+
/// Indicates whether responses should contain a jsonapi object that contains the highest JSON:API version supported. False by default.
39+
/// </summary>
40+
bool IncludeJsonApiVersion { get; }
41+
3742
/// <summary>
3843
/// Whether or not <see cref="Exception" /> stack traces should be serialized in <see cref="ErrorMeta" /> objects. False by default.
3944
/// </summary>

src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ public sealed class JsonApiOptions : IJsonApiOptions
2222
/// <inheritdoc />
2323
public AttrCapabilities DefaultAttrCapabilities { get; set; } = AttrCapabilities.All;
2424

25+
/// <inheritdoc />
26+
public bool IncludeJsonApiVersion { get; set; }
27+
2528
/// <inheritdoc />
2629
public bool IncludeExceptionStackTraceInErrors { get; set; }
2730

src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,18 @@ private string SerializeOperationsDocument(IEnumerable<OperationContainer> opera
6767
Meta = _metaBuilder.Build()
6868
};
6969

70+
if (_options.IncludeJsonApiVersion)
71+
{
72+
document.JsonApi = new JsonApiObject
73+
{
74+
Version = "1.1",
75+
Ext = new List<string>
76+
{
77+
"https://jsonapi.org/ext/atomic"
78+
}
79+
};
80+
}
81+
7082
return SerializeObject(document, _options.SerializerSettings);
7183
}
7284

src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationsDocument.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public sealed class AtomicOperationsDocument
1818
/// See "jsonapi" in https://jsonapi.org/format/#document-top-level.
1919
/// </summary>
2020
[JsonProperty("jsonapi", NullValueHandling = NullValueHandling.Ignore)]
21-
public IDictionary<string, object> JsonApi { get; set; }
21+
public JsonApiObject JsonApi { get; set; }
2222

2323
/// <summary>
2424
/// See "links" in https://jsonapi.org/format/#document-top-level.

src/JsonApiDotNetCore/Serialization/Objects/Document.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public sealed class Document : ExposableData<ResourceObject>
1818
/// see "jsonapi" in https://jsonapi.org/format/#document-top-level
1919
/// </summary>
2020
[JsonProperty("jsonapi", NullValueHandling = NullValueHandling.Ignore)]
21-
public IDictionary<string, object> JsonApi { get; set; }
21+
public JsonApiObject JsonApi { get; set; }
2222

2323
/// <summary>
2424
/// see "links" in https://jsonapi.org/format/#document-top-level
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System.Collections.Generic;
2+
using Newtonsoft.Json;
3+
4+
namespace JsonApiDotNetCore.Serialization.Objects
5+
{
6+
/// <summary>
7+
/// https://jsonapi.org/format/1.1/#document-jsonapi-object.
8+
/// </summary>
9+
public sealed class JsonApiObject
10+
{
11+
[JsonProperty("version", NullValueHandling = NullValueHandling.Ignore)]
12+
public string Version { get; set; }
13+
14+
[JsonProperty("ext", NullValueHandling = NullValueHandling.Ignore)]
15+
public ICollection<string> Ext { get; set; }
16+
17+
[JsonProperty("profile", NullValueHandling = NullValueHandling.Ignore)]
18+
public ICollection<string> Profile { get; set; }
19+
20+
/// <summary>
21+
/// see "meta" in https://jsonapi.org/format/1.1/#document-meta
22+
/// </summary>
23+
[JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)]
24+
public IDictionary<string, object> Meta { get; set; }
25+
}
26+
}

src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,14 @@ internal string SerializeMany(IReadOnlyCollection<IIdentifiable> resources)
149149
/// </summary>
150150
private void AddTopLevelObjects(Document document)
151151
{
152+
if (_options.IncludeJsonApiVersion)
153+
{
154+
document.JsonApi = new JsonApiObject
155+
{
156+
Version = "1.1"
157+
};
158+
}
159+
152160
document.Links = _linkBuilder.GetTopLevelLinks();
153161
document.Meta = _metaBuilder.Build();
154162
document.Included = _includedBuilder.Build();
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
using System.Net;
2+
using System.Net.Http;
3+
using System.Threading.Tasks;
4+
using FluentAssertions;
5+
using JsonApiDotNetCore.Configuration;
6+
using JsonApiDotNetCore.Resources;
7+
using JsonApiDotNetCoreExample.Controllers;
8+
using JsonApiDotNetCoreExampleTests.Startups;
9+
using Microsoft.Extensions.DependencyInjection;
10+
using TestBuildingBlocks;
11+
using Xunit;
12+
13+
namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.Mixed
14+
{
15+
public sealed class AtomicSerializationTests : IClassFixture<ExampleIntegrationTestContext<TestableStartup<OperationsDbContext>, OperationsDbContext>>
16+
{
17+
private readonly ExampleIntegrationTestContext<TestableStartup<OperationsDbContext>, OperationsDbContext> _testContext;
18+
private readonly OperationsFakers _fakers = new OperationsFakers();
19+
20+
public AtomicSerializationTests(ExampleIntegrationTestContext<TestableStartup<OperationsDbContext>, OperationsDbContext> testContext)
21+
{
22+
_testContext = testContext;
23+
24+
testContext.UseController<OperationsController>();
25+
26+
testContext.ConfigureServicesAfterStartup(services =>
27+
{
28+
services.AddScoped(typeof(IResourceChangeTracker<>), typeof(NeverSameResourceChangeTracker<>));
29+
});
30+
31+
var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService<IJsonApiOptions>();
32+
options.IncludeJsonApiVersion = true;
33+
options.AllowClientGeneratedIds = true;
34+
}
35+
36+
[Fact]
37+
public async Task Includes_version_with_ext_on_operations_endpoint()
38+
{
39+
// Arrange
40+
const int newArtistId = 12345;
41+
string newArtistName = _fakers.Performer.Generate().ArtistName;
42+
43+
await _testContext.RunOnDatabaseAsync(async dbContext =>
44+
{
45+
await dbContext.ClearTableAsync<Performer>();
46+
});
47+
48+
var requestBody = new
49+
{
50+
atomic__operations = new[]
51+
{
52+
new
53+
{
54+
op = "add",
55+
data = new
56+
{
57+
type = "performers",
58+
id = newArtistId,
59+
attributes = new
60+
{
61+
artistName = newArtistName
62+
}
63+
}
64+
}
65+
}
66+
};
67+
68+
const string route = "/operations";
69+
70+
// Act
71+
(HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync<string>(route, requestBody);
72+
73+
// Assert
74+
httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
75+
76+
responseDocument.Should().BeJson(@"{
77+
""jsonapi"": {
78+
""version"": ""1.1"",
79+
""ext"": [
80+
""https://jsonapi.org/ext/atomic""
81+
]
82+
},
83+
""atomic:results"": [
84+
{
85+
""data"": {
86+
""type"": ""performers"",
87+
""id"": """ + newArtistId + @""",
88+
""attributes"": {
89+
""artistName"": """ + newArtistName + @""",
90+
""bornAt"": ""0001-01-01T01:00:00+01:00""
91+
},
92+
""links"": {
93+
""self"": ""http://localhost/performers/" + newArtistId + @"""
94+
}
95+
}
96+
}
97+
]
98+
}");
99+
}
100+
}
101+
}

test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/SerializationTests.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public SerializationTests(ExampleIntegrationTestContext<TestableStartup<Serializ
3737
var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService<IJsonApiOptions>();
3838
options.IncludeExceptionStackTraceInErrors = false;
3939
options.AllowClientGeneratedIds = true;
40+
options.IncludeJsonApiVersion = false;
4041
}
4142

4243
[Fact]
@@ -593,6 +594,40 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
593594
""self"": ""http://localhost/meetingAttendees/" + existingAttendee.StringId + @"""
594595
}
595596
}
597+
}");
598+
}
599+
600+
[Fact]
601+
public async Task Includes_version_on_resource_endpoint()
602+
{
603+
// Arrange
604+
var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService<IJsonApiOptions>();
605+
options.IncludeJsonApiVersion = true;
606+
607+
MeetingAttendee attendee = _fakers.MeetingAttendee.Generate();
608+
609+
await _testContext.RunOnDatabaseAsync(async dbContext =>
610+
{
611+
dbContext.Attendees.Add(attendee);
612+
await dbContext.SaveChangesAsync();
613+
});
614+
615+
string route = $"/meetingAttendees/{attendee.StringId}/meeting";
616+
617+
// Act
618+
(HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteGetAsync<string>(route);
619+
620+
// Assert
621+
httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
622+
623+
responseDocument.Should().BeJson(@"{
624+
""jsonapi"": {
625+
""version"": ""1.1""
626+
},
627+
""links"": {
628+
""self"": ""http://localhost/meetingAttendees/" + attendee.StringId + @"/meeting""
629+
},
630+
""data"": null
596631
}");
597632
}
598633
}

0 commit comments

Comments
 (0)