Skip to content

Adds option to emit jsonapi version in response documents #992

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ internal NamingStrategy SerializerNamingStrategy
/// </summary>
AttrCapabilities DefaultAttrCapabilities { get; }

/// <summary>
/// Indicates whether responses should contain a jsonapi object that contains the highest JSON:API version supported. False by default.
/// </summary>
bool IncludeJsonApiVersion { get; }

/// <summary>
/// Whether or not <see cref="Exception" /> stack traces should be serialized in <see cref="ErrorMeta" /> objects. False by default.
/// </summary>
Expand Down
3 changes: 3 additions & 0 deletions src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ public sealed class JsonApiOptions : IJsonApiOptions
/// <inheritdoc />
public AttrCapabilities DefaultAttrCapabilities { get; set; } = AttrCapabilities.All;

/// <inheritdoc />
public bool IncludeJsonApiVersion { get; set; }

/// <inheritdoc />
public bool IncludeExceptionStackTraceInErrors { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,18 @@ private string SerializeOperationsDocument(IEnumerable<OperationContainer> opera
Meta = _metaBuilder.Build()
};

if (_options.IncludeJsonApiVersion)
{
document.JsonApi = new JsonApiObject
{
Version = "1.1",
Ext = new List<string>
{
"https://jsonapi.org/ext/atomic"
}
};
}

return SerializeObject(document, _options.SerializerSettings);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public sealed class AtomicOperationsDocument
/// See "jsonapi" in https://jsonapi.org/format/#document-top-level.
/// </summary>
[JsonProperty("jsonapi", NullValueHandling = NullValueHandling.Ignore)]
public IDictionary<string, object> JsonApi { get; set; }
public JsonApiObject JsonApi { get; set; }

/// <summary>
/// See "links" in https://jsonapi.org/format/#document-top-level.
Expand Down
2 changes: 1 addition & 1 deletion src/JsonApiDotNetCore/Serialization/Objects/Document.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public sealed class Document : ExposableData<ResourceObject>
/// see "jsonapi" in https://jsonapi.org/format/#document-top-level
/// </summary>
[JsonProperty("jsonapi", NullValueHandling = NullValueHandling.Ignore)]
public IDictionary<string, object> JsonApi { get; set; }
public JsonApiObject JsonApi { get; set; }

/// <summary>
/// see "links" in https://jsonapi.org/format/#document-top-level
Expand Down
26 changes: 26 additions & 0 deletions src/JsonApiDotNetCore/Serialization/Objects/JsonApiObject.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Collections.Generic;
using Newtonsoft.Json;

namespace JsonApiDotNetCore.Serialization.Objects
{
/// <summary>
/// https://jsonapi.org/format/1.1/#document-jsonapi-object.
/// </summary>
public sealed class JsonApiObject
{
[JsonProperty("version", NullValueHandling = NullValueHandling.Ignore)]
public string Version { get; set; }

[JsonProperty("ext", NullValueHandling = NullValueHandling.Ignore)]
public ICollection<string> Ext { get; set; }

[JsonProperty("profile", NullValueHandling = NullValueHandling.Ignore)]
public ICollection<string> Profile { get; set; }

/// <summary>
/// see "meta" in https://jsonapi.org/format/1.1/#document-meta
/// </summary>
[JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)]
public IDictionary<string, object> Meta { get; set; }
}
}
8 changes: 8 additions & 0 deletions src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,14 @@ internal string SerializeMany(IReadOnlyCollection<IIdentifiable> resources)
/// </summary>
private void AddTopLevelObjects(Document document)
{
if (_options.IncludeJsonApiVersion)
{
document.JsonApi = new JsonApiObject
{
Version = "1.1"
};
}

document.Links = _linkBuilder.GetTopLevelLinks();
document.Meta = _metaBuilder.Build();
document.Included = _includedBuilder.Build();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using FluentAssertions;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCoreExample.Controllers;
using JsonApiDotNetCoreExampleTests.Startups;
using Microsoft.Extensions.DependencyInjection;
using TestBuildingBlocks;
using Xunit;

namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.Mixed
{
public sealed class AtomicSerializationTests : IClassFixture<ExampleIntegrationTestContext<TestableStartup<OperationsDbContext>, OperationsDbContext>>
{
private readonly ExampleIntegrationTestContext<TestableStartup<OperationsDbContext>, OperationsDbContext> _testContext;
private readonly OperationsFakers _fakers = new OperationsFakers();

public AtomicSerializationTests(ExampleIntegrationTestContext<TestableStartup<OperationsDbContext>, OperationsDbContext> testContext)
{
_testContext = testContext;

testContext.UseController<OperationsController>();

testContext.ConfigureServicesAfterStartup(services =>
{
services.AddScoped(typeof(IResourceChangeTracker<>), typeof(NeverSameResourceChangeTracker<>));
});

var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService<IJsonApiOptions>();
options.IncludeJsonApiVersion = true;
options.AllowClientGeneratedIds = true;
}

[Fact]
public async Task Includes_version_with_ext_on_operations_endpoint()
{
// Arrange
const int newArtistId = 12345;
string newArtistName = _fakers.Performer.Generate().ArtistName;

await _testContext.RunOnDatabaseAsync(async dbContext =>
{
await dbContext.ClearTableAsync<Performer>();
});

var requestBody = new
{
atomic__operations = new[]
{
new
{
op = "add",
data = new
{
type = "performers",
id = newArtistId,
attributes = new
{
artistName = newArtistName
}
}
}
}
};

const string route = "/operations";

// Act
(HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync<string>(route, requestBody);

// Assert
httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);

responseDocument.Should().BeJson(@"{
""jsonapi"": {
""version"": ""1.1"",
""ext"": [
""https://jsonapi.org/ext/atomic""
]
},
""atomic:results"": [
{
""data"": {
""type"": ""performers"",
""id"": """ + newArtistId + @""",
""attributes"": {
""artistName"": """ + newArtistName + @""",
""bornAt"": ""0001-01-01T01:00:00+01:00""
},
""links"": {
""self"": ""http://localhost/performers/" + newArtistId + @"""
}
}
}
]
}");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public SerializationTests(ExampleIntegrationTestContext<TestableStartup<Serializ
var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService<IJsonApiOptions>();
options.IncludeExceptionStackTraceInErrors = false;
options.AllowClientGeneratedIds = true;
options.IncludeJsonApiVersion = false;
}

[Fact]
Expand Down Expand Up @@ -555,6 +556,40 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
""self"": ""http://localhost/meetingAttendees/" + existingAttendee.StringId + @"""
}
}
}");
}

[Fact]
public async Task Includes_version_on_resource_endpoint()
{
// Arrange
var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService<IJsonApiOptions>();
options.IncludeJsonApiVersion = true;

MeetingAttendee attendee = _fakers.MeetingAttendee.Generate();

await _testContext.RunOnDatabaseAsync(async dbContext =>
{
dbContext.Attendees.Add(attendee);
await dbContext.SaveChangesAsync();
});

string route = $"/meetingAttendees/{attendee.StringId}/meeting";

// Act
(HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteGetAsync<string>(route);

// Assert
httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);

responseDocument.Should().BeJson(@"{
""jsonapi"": {
""version"": ""1.1""
},
""links"": {
""self"": ""http://localhost/meetingAttendees/" + attendee.StringId + @"/meeting""
},
""data"": null
}");
}
}
Expand Down