Skip to content

Commit 04ba8b1

Browse files
author
Bart Koelman
authoredFeb 10, 2021
Fixed: Global options not respected in relationship links rendering (#946)
* Bugfix: relationship links were rendered for ToMany relationships, even when explicitly turned off in global options (and not configured on the relationship itself). * Added tests and clarified documentation. Removed verification logic in value setters because this is a flags enum, so the checks were incorrect. For example, you are not allowed to pass `Links.Paging`, but are allowed to pass `Links.Related | Links.Paging` or `Links.All`, which all mean "include paging in the set of links".
1 parent 9fe260e commit 04ba8b1

File tree

17 files changed

+575
-486
lines changed

17 files changed

+575
-486
lines changed
 

‎src/Examples/JsonApiDotNetCoreExample/Definitions/PassportHooksDefinition.cs

-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using JsonApiDotNetCore.Configuration;
55
using JsonApiDotNetCore.Errors;
66
using JsonApiDotNetCore.Hooks.Internal.Execution;
7-
using JsonApiDotNetCore.Resources;
87
using JsonApiDotNetCore.Serialization.Objects;
98
using JsonApiDotNetCoreExample.Models;
109

‎src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs

+15-28
Original file line numberDiff line numberDiff line change
@@ -56,41 +56,28 @@ public interface IJsonApiOptions
5656
bool UseRelativeLinks { get; }
5757

5858
/// <summary>
59-
/// Configures globally which links to show in the <see cref="Serialization.Objects.TopLevelLinks"/>
60-
/// object for a requested resource. Setting can be overridden per resource by
61-
/// adding a <see cref="ResourceLinksAttribute"/> to the class definition of that resource.
59+
/// Configures which links to show in the <see cref="Serialization.Objects.TopLevelLinks"/>
60+
/// object. Defaults to <see cref="LinkTypes.All"/>.
61+
/// This setting can be overruled per resource type by
62+
/// adding <see cref="ResourceLinksAttribute"/> on the class definition of a resource.
6263
/// </summary>
6364
LinkTypes TopLevelLinks { get; }
6465

6566
/// <summary>
66-
/// Configures globally which links to show in the <see cref="Serialization.Objects.ResourceLinks"/>
67-
/// object for a requested resource. Setting can be overridden per resource by
68-
/// adding a <see cref="ResourceLinksAttribute"/> to the class definition of that resource.
67+
/// Configures which links to show in the <see cref="Serialization.Objects.ResourceLinks"/>
68+
/// object. Defaults to <see cref="LinkTypes.All"/>.
69+
/// This setting can be overruled per resource type by
70+
/// adding <see cref="ResourceLinksAttribute"/> on the class definition of a resource.
6971
/// </summary>
7072
LinkTypes ResourceLinks { get; }
7173

7274
/// <summary>
73-
/// Configures globally which links to show in the <see cref="Serialization.Objects.RelationshipLinks"/>
74-
/// object for a requested resource. Setting can be overridden per resource by
75-
/// adding a <see cref="ResourceLinksAttribute"/> to the class definition of that resource.
76-
/// This option can also be specified per relationship by using the associated links argument
77-
/// in the constructor of <see cref="RelationshipAttribute"/>.
75+
/// Configures which links to show in the <see cref="Serialization.Objects.RelationshipLinks"/>
76+
/// object. Defaults to <see cref="LinkTypes.All"/>.
77+
/// This setting can be overruled for all relationships per resource type by
78+
/// adding <see cref="ResourceLinksAttribute"/> on the class definition of a resource.
79+
/// This can be further overruled per relationship by setting <see cref="RelationshipAttribute.Links"/>.
7880
/// </summary>
79-
/// <example>
80-
/// <code>
81-
/// options.RelationshipLinks = LinkTypes.None;
82-
/// </code>
83-
/// <code>
84-
/// {
85-
/// "type": "articles",
86-
/// "id": "4309",
87-
/// "relationships": {
88-
/// "author": { "data": { "type": "people", "id": "1234" }
89-
/// }
90-
/// }
91-
/// }
92-
/// </code>
93-
/// </example>
9481
LinkTypes RelationshipLinks { get; }
9582

9683
/// <summary>
@@ -154,13 +141,13 @@ public interface IJsonApiOptions
154141
bool EnableLegacyFilterNotation { get; }
155142

156143
/// <summary>
157-
/// Determines whether the <see cref="JsonSerializerSettings.NullValueHandling"/> serialization setting can be overridden by using a query string parameter.
144+
/// Determines whether the <see cref="JsonSerializerSettings.NullValueHandling"/> serialization setting can be controlled using a query string parameter.
158145
/// False by default.
159146
/// </summary>
160147
bool AllowQueryStringOverrideForSerializerNullValueHandling { get; }
161148

162149
/// <summary>
163-
/// Determines whether the <see cref="JsonSerializerSettings.DefaultValueHandling"/> serialization setting can be overridden by using a query string parameter.
150+
/// Determines whether the <see cref="JsonSerializerSettings.DefaultValueHandling"/> serialization setting can be controlled using a query string parameter.
164151
/// False by default.
165152
/// </summary>
166153
bool AllowQueryStringOverrideForSerializerDefaultValueHandling { get; }

‎src/JsonApiDotNetCore/Configuration/ResourceContext.cs

+16-10
Original file line numberDiff line numberDiff line change
@@ -52,27 +52,33 @@ public class ResourceContext
5252

5353
/// <summary>
5454
/// Configures which links to show in the <see cref="Serialization.Objects.TopLevelLinks"/>
55-
/// object for this resource. If set to <see cref="LinkTypes.NotConfigured"/>,
56-
/// the configuration will be read from <see cref="IJsonApiOptions"/>.
57-
/// Defaults to <see cref="LinkTypes.NotConfigured"/>.
55+
/// object for this resource type.
56+
/// Defaults to <see cref="LinkTypes.NotConfigured"/>, which falls back to <see cref="IJsonApiOptions.TopLevelLinks"/>.
5857
/// </summary>
58+
/// <remarks>
59+
/// In the process of building the resource graph, this value is set based on <see cref="ResourceLinksAttribute.TopLevelLinks"/> usage.
60+
/// </remarks>
5961
public LinkTypes TopLevelLinks { get; internal set; } = LinkTypes.NotConfigured;
6062

6163
/// <summary>
6264
/// Configures which links to show in the <see cref="Serialization.Objects.ResourceLinks"/>
63-
/// object for this resource. If set to <see cref="LinkTypes.NotConfigured"/>,
64-
/// the configuration will be read from <see cref="IJsonApiOptions"/>.
65-
/// Defaults to <see cref="LinkTypes.NotConfigured"/>.
65+
/// object for this resource type.
66+
/// Defaults to <see cref="LinkTypes.NotConfigured"/>, which falls back to <see cref="IJsonApiOptions.ResourceLinks"/>.
6667
/// </summary>
68+
/// <remarks>
69+
/// In the process of building the resource graph, this value is set based on <see cref="ResourceLinksAttribute.ResourceLinks"/> usage.
70+
/// </remarks>
6771
public LinkTypes ResourceLinks { get; internal set; } = LinkTypes.NotConfigured;
6872

6973
/// <summary>
7074
/// Configures which links to show in the <see cref="Serialization.Objects.RelationshipLinks"/>
71-
/// for all relationships of the resource for which this attribute was instantiated.
72-
/// If set to <see cref="LinkTypes.NotConfigured"/>, the configuration will
73-
/// be read from <see cref="RelationshipAttribute.Links"/> or
74-
/// <see cref="IJsonApiOptions"/>. Defaults to <see cref="LinkTypes.NotConfigured"/>.
75+
/// object for all relationships of this resource type.
76+
/// Defaults to <see cref="LinkTypes.NotConfigured"/>, which falls back to <see cref="IJsonApiOptions.RelationshipLinks"/>.
77+
/// This can be overruled per relationship by setting <see cref="RelationshipAttribute.Links"/>.
7578
/// </summary>
79+
/// <remarks>
80+
/// In the process of building the resource graph, this value is set based on <see cref="ResourceLinksAttribute.RelationshipLinks"/> usage.
81+
/// </remarks>
7682
public LinkTypes RelationshipLinks { get; internal set; } = LinkTypes.NotConfigured;
7783

7884
public override string ToString()

‎src/JsonApiDotNetCore/Resources/Annotations/HasManyAttribute.cs

+9-16
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,17 @@ namespace JsonApiDotNetCore.Resources.Annotations
55
/// <summary>
66
/// Used to expose a property on a resource class as a JSON:API to-many relationship (https://jsonapi.org/format/#document-resource-object-relationships).
77
/// </summary>
8+
/// <example>
9+
/// <code><![CDATA[
10+
/// public class Author : Identifiable
11+
/// {
12+
/// [HasMany(PublicName = "articles")]
13+
/// public List<Article> Articles { get; set; }
14+
/// }
15+
/// ]]></code>
16+
/// </example>
817
[AttributeUsage(AttributeTargets.Property)]
918
public class HasManyAttribute : RelationshipAttribute
1019
{
11-
/// <summary>
12-
/// Creates a HasMany relational link to another resource.
13-
/// </summary>
14-
/// <example>
15-
/// <code><![CDATA[
16-
/// public class Author : Identifiable
17-
/// {
18-
/// [HasMany(PublicName = "articles")]
19-
/// public List<Article> Articles { get; set; }
20-
/// }
21-
/// ]]></code>
22-
/// </example>
23-
public HasManyAttribute()
24-
{
25-
Links = LinkTypes.All;
26-
}
2720
}
2821
}

‎src/JsonApiDotNetCore/Resources/Annotations/HasOneAttribute.cs

-4
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,5 @@ namespace JsonApiDotNetCore.Resources.Annotations
88
[AttributeUsage(AttributeTargets.Property)]
99
public sealed class HasOneAttribute : RelationshipAttribute
1010
{
11-
public HasOneAttribute()
12-
{
13-
Links = LinkTypes.NotConfigured;
14-
}
1511
}
1612
}

‎src/JsonApiDotNetCore/Resources/Annotations/RelationshipAttribute.cs

+5-23
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
22
using System.Reflection;
33
using JsonApiDotNetCore.Configuration;
4-
using JsonApiDotNetCore.Errors;
54

65
namespace JsonApiDotNetCore.Resources.Annotations
76
{
@@ -10,8 +9,6 @@ namespace JsonApiDotNetCore.Resources.Annotations
109
/// </summary>
1110
public abstract class RelationshipAttribute : ResourceFieldAttribute
1211
{
13-
private LinkTypes _links;
14-
1512
/// <summary>
1613
/// The property name of the EF Core inverse navigation, which may or may not exist.
1714
/// Even if it exists, it may not be exposed as a JSON:API relationship.
@@ -58,27 +55,12 @@ public abstract class RelationshipAttribute : ResourceFieldAttribute
5855
public Type LeftType { get; internal set; }
5956

6057
/// <summary>
61-
/// Configures which links to show in the <see cref="Links"/> object for this relationship.
62-
/// When not explicitly assigned, the default value depends on the relationship type (see remarks).
58+
/// Configures which links to show in the <see cref="Serialization.Objects.RelationshipLinks"/>
59+
/// object for this relationship.
60+
/// Defaults to <see cref="LinkTypes.NotConfigured"/>, which falls back to <see cref="ResourceLinksAttribute.RelationshipLinks"/>
61+
/// and then falls back to <see cref="IJsonApiOptions.RelationshipLinks"/>.
6362
/// </summary>
64-
/// <remarks>
65-
/// This defaults to <see cref="LinkTypes.All"/> for <see cref="HasManyAttribute"/> and <see cref="HasManyThroughAttribute"/> relationships.
66-
/// This defaults to <see cref="LinkTypes.NotConfigured"/> for <see cref="HasOneAttribute"/> relationships, which means that
67-
/// the configuration in <see cref="IJsonApiOptions"/> or <see cref="ResourceContext"/> is used.
68-
/// </remarks>
69-
public LinkTypes Links
70-
{
71-
get => _links;
72-
set
73-
{
74-
if (value == LinkTypes.Paging)
75-
{
76-
throw new InvalidConfigurationException($"{LinkTypes.Paging:g} not allowed for argument {nameof(value)}");
77-
}
78-
79-
_links = value;
80-
}
81-
}
63+
public LinkTypes Links { get; set; } = LinkTypes.NotConfigured;
8264

8365
/// <summary>
8466
/// Whether or not this relationship can be included using the <c>?include=publicName</c> query string parameter.
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,34 @@
11
using System;
2-
using JsonApiDotNetCore.Errors;
2+
using JsonApiDotNetCore.Configuration;
33

44
namespace JsonApiDotNetCore.Resources.Annotations
55
{
6-
// TODO: There are no tests for this.
7-
86
/// <summary>
97
/// When put on a resource class, overrides global configuration for which links to render.
108
/// </summary>
119
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)]
1210
public sealed class ResourceLinksAttribute : Attribute
1311
{
14-
private LinkTypes _topLevelLinks = LinkTypes.NotConfigured;
15-
private LinkTypes _resourceLinks = LinkTypes.NotConfigured;
16-
private LinkTypes _relationshipLinks = LinkTypes.NotConfigured;
17-
1812
/// <summary>
1913
/// Configures which links to show in the <see cref="Serialization.Objects.TopLevelLinks"/>
20-
/// section for this resource.
21-
/// Defaults to <see cref="LinkTypes.NotConfigured"/>.
14+
/// object for this resource type.
15+
/// Defaults to <see cref="LinkTypes.NotConfigured"/>, which falls back to <see cref="IJsonApiOptions.TopLevelLinks"/>.
2216
/// </summary>
23-
public LinkTypes TopLevelLinks
24-
{
25-
get => _topLevelLinks;
26-
set
27-
{
28-
if (value == LinkTypes.Related)
29-
{
30-
throw new InvalidConfigurationException($"{LinkTypes.Related:g} not allowed for argument {nameof(value)}");
31-
}
32-
33-
_topLevelLinks = value;
34-
}
35-
}
17+
public LinkTypes TopLevelLinks { get; set; } = LinkTypes.NotConfigured;
3618

3719
/// <summary>
3820
/// Configures which links to show in the <see cref="Serialization.Objects.ResourceLinks"/>
39-
/// section for this resource.
40-
/// Defaults to <see cref="LinkTypes.NotConfigured"/>.
21+
/// object for this resource type.
22+
/// Defaults to <see cref="LinkTypes.NotConfigured"/>, which falls back to <see cref="IJsonApiOptions.ResourceLinks"/>.
4123
/// </summary>
42-
public LinkTypes ResourceLinks
43-
{
44-
get => _resourceLinks;
45-
set
46-
{
47-
if (value == LinkTypes.Paging)
48-
{
49-
throw new InvalidConfigurationException($"{LinkTypes.Paging:g} not allowed for argument {nameof(value)}");
50-
}
51-
52-
_resourceLinks = value;
53-
}
54-
}
55-
24+
public LinkTypes ResourceLinks { get; set; } = LinkTypes.NotConfigured;
25+
5626
/// <summary>
5727
/// Configures which links to show in the <see cref="Serialization.Objects.RelationshipLinks"/>
58-
/// for all relationships of the resource type on which this attribute was used.
59-
/// Defaults to <see cref="LinkTypes.NotConfigured"/>.
28+
/// object for all relationships of this resource type.
29+
/// Defaults to <see cref="LinkTypes.NotConfigured"/>, which falls back to <see cref="IJsonApiOptions.RelationshipLinks"/>.
30+
/// This can be overruled per relationship by setting <see cref="RelationshipAttribute.Links"/>.
6031
/// </summary>
61-
public LinkTypes RelationshipLinks
62-
{
63-
get => _relationshipLinks;
64-
set
65-
{
66-
if (value == LinkTypes.Paging)
67-
{
68-
throw new InvalidConfigurationException($"{LinkTypes.Paging:g} not allowed for argument {nameof(value)}");
69-
}
70-
71-
_relationshipLinks = value;
72-
}
73-
}
32+
public LinkTypes RelationshipLinks { get; set; } = LinkTypes.NotConfigured;
7433
}
7534
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using System.Net;
2+
using System.Threading.Tasks;
3+
using FluentAssertions;
4+
using JsonApiDotNetCore.Serialization.Objects;
5+
using JsonApiDotNetCoreExampleTests.Startups;
6+
using TestBuildingBlocks;
7+
using Xunit;
8+
9+
namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Links
10+
{
11+
public sealed class LinkInclusionTests
12+
: IClassFixture<ExampleIntegrationTestContext<TestableStartup<LinksDbContext>, LinksDbContext>>
13+
{
14+
private readonly ExampleIntegrationTestContext<TestableStartup<LinksDbContext>, LinksDbContext> _testContext;
15+
private readonly LinksFakers _fakers = new LinksFakers();
16+
17+
public LinkInclusionTests(ExampleIntegrationTestContext<TestableStartup<LinksDbContext>, LinksDbContext> testContext)
18+
{
19+
_testContext = testContext;
20+
}
21+
22+
[Fact]
23+
public async Task Get_primary_resource_with_include_applies_links_visibility_from_ResourceLinksAttribute()
24+
{
25+
// Arrange
26+
var location = _fakers.PhotoLocation.Generate();
27+
location.Photo = _fakers.Photo.Generate();
28+
location.Album = _fakers.PhotoAlbum.Generate();
29+
30+
await _testContext.RunOnDatabaseAsync(async dbContext =>
31+
{
32+
dbContext.PhotoLocations.Add(location);
33+
await dbContext.SaveChangesAsync();
34+
});
35+
36+
var route = $"/photoLocations/{location.StringId}?include=photo,album";
37+
38+
// Act
39+
var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync<Document>(route);
40+
41+
// Assert
42+
httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
43+
44+
responseDocument.Links.Should().BeNull();
45+
46+
responseDocument.SingleData.Should().NotBeNull();
47+
responseDocument.SingleData.Links.Should().BeNull();
48+
responseDocument.SingleData.Relationships["photo"].Links.Self.Should().BeNull();
49+
responseDocument.SingleData.Relationships["photo"].Links.Related.Should().NotBeNull();
50+
responseDocument.SingleData.Relationships["album"].Links.Should().BeNull();
51+
52+
responseDocument.Included.Should().HaveCount(2);
53+
54+
responseDocument.Included[0].Links.Self.Should().NotBeNull();
55+
responseDocument.Included[0].Relationships["location"].Links.Self.Should().NotBeNull();
56+
responseDocument.Included[0].Relationships["location"].Links.Related.Should().NotBeNull();
57+
58+
responseDocument.Included[1].Links.Self.Should().NotBeNull();
59+
responseDocument.Included[1].Relationships["photos"].Links.Self.Should().NotBeNull();
60+
responseDocument.Included[1].Relationships["photos"].Links.Related.Should().NotBeNull();
61+
}
62+
}
63+
}

‎test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/LinksDbContext.cs

+9
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,19 @@ public sealed class LinksDbContext : DbContext
66
{
77
public DbSet<PhotoAlbum> PhotoAlbums { get; set; }
88
public DbSet<Photo> Photos { get; set; }
9+
public DbSet<PhotoLocation> PhotoLocations { get; set; }
910

1011
public LinksDbContext(DbContextOptions<LinksDbContext> options)
1112
: base(options)
1213
{
1314
}
15+
16+
protected override void OnModelCreating(ModelBuilder builder)
17+
{
18+
builder.Entity<Photo>()
19+
.HasOne(photo => photo.Location)
20+
.WithOne(location => location.Photo)
21+
.HasForeignKey<Photo>("PhotoLocationKey");
22+
}
1423
}
1524
}

0 commit comments

Comments
 (0)
Please sign in to comment.