Skip to content

Commit af46168

Browse files
authored
Merge pull request #1197 from json-api-dotnet/relationship-capabilities
Relationship capabilities and bugfixes
2 parents 01e132b + 4b725bd commit af46168

File tree

77 files changed

+1793
-317
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+1793
-317
lines changed

Diff for: docs/usage/extensibility/resource-definitions.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ from Entity Framework Core `IQueryable` execution.
3434

3535
### Excluding fields
3636

37-
There are some cases where you want attributes (or relationships) conditionally excluded from your resource response.
37+
There are some cases where you want attributes or relationships conditionally excluded from your resource response.
3838
For example, you may accept some sensitive data that should only be exposed to administrators after creation.
3939

40-
**Note:** to exclude attributes unconditionally, use `[Attr(Capabilities = ~AttrCapabilities.AllowView)]` on a resource class property.
40+
**Note:** to exclude fields unconditionally, [attribute capabilities](~/usage/resources/attributes.md#capabilities) and [relationship capabilities](~/usage/resources/relationships.md#capabilities) can be used instead.
4141

4242
```c#
4343
public class UserDefinition : JsonApiResourceDefinition<User, int>

Diff for: docs/usage/resources/attributes.md

+29-14
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,10 @@ options.DefaultAttrCapabilities = AttrCapabilities.None; // default: All
4343

4444
This can be overridden per attribute.
4545

46-
### Viewability
46+
### AllowView
4747

48-
Attributes can be marked to allow returning their value in responses. When not allowed and requested using `?fields[]=`, it results in an HTTP 400 response.
48+
Indicates whether the attribute value can be returned in responses. When not allowed and requested using `?fields[]=`, it results in an HTTP 400 response.
49+
Otherwise, the attribute is silently omitted.
4950

5051
```c#
5152
#nullable enable
@@ -57,45 +58,59 @@ public class User : Identifiable<int>
5758
}
5859
```
5960

60-
### Creatability
61+
### AllowFilter
6162

62-
Attributes can be marked as creatable, which will allow `POST` requests to assign a value to them. When sent but not allowed, an HTTP 422 response is returned.
63+
Indicates whether the attribute can be filtered on. When not allowed and used in `?filter=`, an HTTP 400 is returned.
6364

6465
```c#
6566
#nullable enable
6667

6768
public class Person : Identifiable<int>
6869
{
69-
[Attr(Capabilities = AttrCapabilities.AllowCreate)]
70-
public string? CreatorName { get; set; }
70+
[Attr(Capabilities = AttrCapabilities.AllowFilter)]
71+
public string? FirstName { get; set; }
7172
}
7273
```
7374

74-
### Changeability
75+
### AllowSort
7576

76-
Attributes can be marked as changeable, which will allow `PATCH` requests to update them. When sent but not allowed, an HTTP 422 response is returned.
77+
Indicates whether the attribute can be sorted on. When not allowed and used in `?sort=`, an HTTP 400 is returned.
7778

7879
```c#
7980
#nullable enable
8081

8182
public class Person : Identifiable<int>
8283
{
83-
[Attr(Capabilities = AttrCapabilities.AllowChange)]
84-
public string? FirstName { get; set; };
84+
[Attr(Capabilities = ~AttrCapabilities.AllowSort)]
85+
public string? FirstName { get; set; }
8586
}
8687
```
8788

88-
### Filter/Sort-ability
89+
### AllowCreate
8990

90-
Attributes can be marked to allow filtering and/or sorting. When not allowed, it results in an HTTP 400 response.
91+
Indicates whether POST requests can assign the attribute value. When sent but not allowed, an HTTP 422 response is returned.
9192

9293
```c#
9394
#nullable enable
9495

9596
public class Person : Identifiable<int>
9697
{
97-
[Attr(Capabilities = AttrCapabilities.AllowSort | AttrCapabilities.AllowFilter)]
98-
public string? FirstName { get; set; }
98+
[Attr(Capabilities = AttrCapabilities.AllowCreate)]
99+
public string? CreatorName { get; set; }
100+
}
101+
```
102+
103+
### AllowChange
104+
105+
Indicates whether PATCH requests can update the attribute value. When sent but not allowed, an HTTP 422 response is returned.
106+
107+
```c#
108+
#nullable enable
109+
110+
public class Person : Identifiable<int>
111+
{
112+
[Attr(Capabilities = AttrCapabilities.AllowChange)]
113+
public string? FirstName { get; set; };
99114
}
100115
```
101116

Diff for: docs/usage/resources/relationships.md

+105-1
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,111 @@ public class TodoItem : Identifiable<int>
160160
}
161161
```
162162

163-
## Includibility
163+
## Capabilities
164+
165+
_since v5.1_
166+
167+
Default JSON:API relationship capabilities are specified in
168+
@JsonApiDotNetCore.Configuration.JsonApiOptions#JsonApiDotNetCore_Configuration_JsonApiOptions_DefaultHasOneCapabilities and
169+
@JsonApiDotNetCore.Configuration.JsonApiOptions#JsonApiDotNetCore_Configuration_JsonApiOptions_DefaultHasManyCapabilities:
170+
171+
```c#
172+
options.DefaultHasOneCapabilities = HasOneCapabilities.None; // default: All
173+
options.DefaultHasManyCapabilities = HasManyCapabilities.None; // default: All
174+
```
175+
176+
This can be overridden per relationship.
177+
178+
### AllowView
179+
180+
Indicates whether the relationship can be returned in responses. When not allowed and requested using `?fields[]=`, it results in an HTTP 400 response.
181+
Otherwise, the relationship (and its related resources, when included) are silently omitted.
182+
183+
Note that this setting does not affect retrieving the related resources directly.
184+
185+
```c#
186+
#nullable enable
187+
188+
public class User : Identifiable<int>
189+
{
190+
[HasOne(Capabilities = ~HasOneCapabilities.AllowView)]
191+
public LoginAccount Account { get; set; } = null!;
192+
}
193+
```
194+
195+
### AllowInclude
196+
197+
Indicates whether the relationship can be included. When not allowed and used in `?include=`, an HTTP 400 is returned.
198+
199+
```c#
200+
#nullable enable
201+
202+
public class User : Identifiable<int>
203+
{
204+
[HasMany(Capabilities = ~HasManyCapabilities.AllowInclude)]
205+
public ISet<Group> Groups { get; set; } = new HashSet<Group>();
206+
}
207+
```
208+
209+
### AllowFilter
210+
211+
For to-many relationships only. Indicates whether it can be used in the `count()` and `has()` filter functions. When not allowed and used in `?filter=`, an HTTP 400 is returned.
212+
213+
```c#
214+
#nullable enable
215+
216+
public class User : Identifiable<int>
217+
{
218+
[HasMany(Capabilities = HasManyCapabilities.AllowFilter)]
219+
public ISet<Group> Groups { get; set; } = new HashSet<Group>();
220+
}
221+
```
222+
223+
### AllowSet
224+
225+
Indicates whether POST and PATCH requests can replace the relationship. When sent but not allowed, an HTTP 422 response is returned.
226+
227+
```c#
228+
#nullable enable
229+
230+
public class User : Identifiable<int>
231+
{
232+
[HasOne(Capabilities = ~HasOneCapabilities.AllowSet)]
233+
public LoginAccount Account { get; set; } = null!;
234+
}
235+
```
236+
237+
### AllowAdd
238+
239+
For to-many relationships only. Indicates whether POST requests can add resources to the relationship. When sent but not allowed, an HTTP 422 response is returned.
240+
241+
```c#
242+
#nullable enable
243+
244+
public class User : Identifiable<int>
245+
{
246+
[HasMany(Capabilities = ~HasManyCapabilities.AllowAdd)]
247+
public ISet<Group> Groups { get; set; } = new HashSet<Group>();
248+
}
249+
```
250+
251+
### AllowRemove
252+
253+
For to-many relationships only. Indicates whether DELETE requests can remove resources from the relationship. When sent but not allowed, an HTTP 422 response is returned.
254+
255+
```c#
256+
#nullable enable
257+
258+
public class User : Identifiable<int>
259+
{
260+
[HasMany(Capabilities = ~HasManyCapabilities.AllowRemove)]
261+
public ISet<Group> Groups { get; set; } = new HashSet<Group>();
262+
}
263+
```
264+
265+
## CanInclude
266+
267+
_obsolete since v5.1_
164268

165269
Relationships can be marked to disallow including them using the `?include=` query string parameter. When not allowed, it results in an HTTP 400 response.
166270

Diff for: src/Examples/JsonApiDotNetCoreExample/Models/TodoItem.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ public sealed class TodoItem : Identifiable<int>
2525
[HasOne]
2626
public Person Owner { get; set; } = null!;
2727

28-
[HasOne]
28+
[HasOne(Capabilities = HasOneCapabilities.AllowView | HasOneCapabilities.AllowSet)]
2929
public Person? Assignee { get; set; }
3030

31-
[HasMany]
31+
[HasMany(Capabilities = HasManyCapabilities.AllowView | HasManyCapabilities.AllowFilter)]
3232
public ISet<Tag> Tags { get; set; } = new HashSet<Tag>();
3333
}

Diff for: src/JsonApiDotNetCore.Annotations/JsonApiDotNetCore.Annotations.csproj

+6-1
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
<PropertyGroup>
33
<TargetFrameworks>$(TargetFrameworkName);netstandard1.0</TargetFrameworks>
44
<IsPackable>true</IsPackable>
@@ -45,4 +45,9 @@
4545
<Compile Remove="**/*.netstandard.cs" />
4646
<None Include="**/*.netstandard.cs" />
4747
</ItemGroup>
48+
49+
<ItemGroup>
50+
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
51+
<PackageReference Include="SauceControl.InheritDoc" Version="1.3.0" PrivateAssets="All" />
52+
</ItemGroup>
4853
</Project>

Diff for: src/JsonApiDotNetCore.Annotations/Resources/Annotations/AttrAttribute.cs

+5-6
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,16 @@ public sealed class AttrAttribute : ResourceFieldAttribute
1414
internal bool HasExplicitCapabilities => _capabilities != null;
1515

1616
/// <summary>
17-
/// The set of capabilities that are allowed to be performed on this attribute. When not explicitly assigned, the configured default set of capabilities
18-
/// is used.
17+
/// The set of allowed capabilities on this attribute. When not explicitly set, the configured default set of capabilities is used.
1918
/// </summary>
2019
/// <example>
21-
/// <code>
22-
/// public class Author : Identifiable
20+
/// <code><![CDATA[
21+
/// public class Author : Identifiable<long>
2322
/// {
2423
/// [Attr(Capabilities = AttrCapabilities.AllowFilter | AttrCapabilities.AllowSort)]
25-
/// public string Name { get; set; }
24+
/// public string Name { get; set; } = null!;
2625
/// }
27-
/// </code>
26+
/// ]]></code>
2827
/// </example>
2928
public AttrCapabilities Capabilities
3029
{

Diff for: src/JsonApiDotNetCore.Annotations/Resources/Annotations/AttrCapabilities.shared.cs

+11-8
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace JsonApiDotNetCore.Resources.Annotations;
44

55
/// <summary>
6-
/// Indicates capabilities that can be performed on an <see cref="AttrAttribute" />.
6+
/// Indicates what can be performed on an <see cref="AttrAttribute" />.
77
/// </summary>
88
[PublicAPI]
99
[Flags]
@@ -12,29 +12,32 @@ public enum AttrCapabilities
1212
None = 0,
1313

1414
/// <summary>
15-
/// Whether or not GET requests can retrieve the attribute. Attempts to retrieve when disabled will return an HTTP 400 response.
15+
/// Whether or not the attribute value can be returned in responses. Attempts to explicitly request it via the <c>fields</c> query string parameter when
16+
/// disabled will return an HTTP 400 response. Otherwise, the attribute is silently omitted.
1617
/// </summary>
1718
AllowView = 1,
1819

1920
/// <summary>
2021
/// Whether or not POST requests can assign the attribute value. Attempts to assign when disabled will return an HTTP 422 response.
2122
/// </summary>
22-
AllowCreate = 2,
23+
AllowCreate = 1 << 1,
2324

2425
/// <summary>
2526
/// Whether or not PATCH requests can update the attribute value. Attempts to update when disabled will return an HTTP 422 response.
2627
/// </summary>
27-
AllowChange = 4,
28+
AllowChange = 1 << 2,
2829

2930
/// <summary>
30-
/// Whether or not an attribute can be filtered on via a query string parameter. Attempts to filter when disabled will return an HTTP 400 response.
31+
/// Whether or not the attribute can be filtered on. Attempts to use it in the <c>filter</c> query string parameter when disabled will return an HTTP 400
32+
/// response.
3133
/// </summary>
32-
AllowFilter = 8,
34+
AllowFilter = 1 << 3,
3335

3436
/// <summary>
35-
/// Whether or not an attribute can be sorted on via a query string parameter. Attempts to sort when disabled will return an HTTP 400 response.
37+
/// Whether or not the attribute can be sorted on. Attempts to use it in the <c>sort</c> query string parameter when disabled will return an HTTP 400
38+
/// response.
3639
/// </summary>
37-
AllowSort = 16,
40+
AllowSort = 1 << 4,
3841

3942
All = AllowView | AllowCreate | AllowChange | AllowFilter | AllowSort
4043
}

Diff for: src/JsonApiDotNetCore.Annotations/Resources/Annotations/HasManyAttribute.cs

+45
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using JetBrains.Annotations;
22

3+
// ReSharper disable NonReadonlyMemberInGetHashCode
4+
35
namespace JsonApiDotNetCore.Resources.Annotations;
46

57
/// <summary>
@@ -20,12 +22,33 @@ namespace JsonApiDotNetCore.Resources.Annotations;
2022
public sealed class HasManyAttribute : RelationshipAttribute
2123
{
2224
private readonly Lazy<bool> _lazyIsManyToMany;
25+
private HasManyCapabilities? _capabilities;
2326

2427
/// <summary>
2528
/// Inspects <see cref="RelationshipAttribute.InverseNavigationProperty" /> to determine if this is a many-to-many relationship.
2629
/// </summary>
2730
internal bool IsManyToMany => _lazyIsManyToMany.Value;
2831

32+
internal bool HasExplicitCapabilities => _capabilities != null;
33+
34+
/// <summary>
35+
/// The set of allowed capabilities on this to-many relationship. When not explicitly set, the configured default set of capabilities is used.
36+
/// </summary>
37+
/// <example>
38+
/// <code><![CDATA[
39+
/// public class Book : Identifiable<long>
40+
/// {
41+
/// [HasMany(Capabilities = HasManyCapabilities.AllowView | HasManyCapabilities.AllowInclude)]
42+
/// public ISet<Chapter> Chapters { get; set; } = new HashSet<Chapter>();
43+
/// }
44+
/// ]]></code>
45+
/// </example>
46+
public HasManyCapabilities Capabilities
47+
{
48+
get => _capabilities ?? default;
49+
set => _capabilities = value;
50+
}
51+
2952
public HasManyAttribute()
3053
{
3154
_lazyIsManyToMany = new Lazy<bool>(EvaluateIsManyToMany, LazyThreadSafetyMode.PublicationOnly);
@@ -41,4 +64,26 @@ private bool EvaluateIsManyToMany()
4164

4265
return false;
4366
}
67+
68+
public override bool Equals(object? obj)
69+
{
70+
if (ReferenceEquals(this, obj))
71+
{
72+
return true;
73+
}
74+
75+
if (obj is null || GetType() != obj.GetType())
76+
{
77+
return false;
78+
}
79+
80+
var other = (HasManyAttribute)obj;
81+
82+
return _capabilities == other._capabilities && base.Equals(other);
83+
}
84+
85+
public override int GetHashCode()
86+
{
87+
return HashCode.Combine(_capabilities, base.GetHashCode());
88+
}
4489
}

Diff for: src/JsonApiDotNetCore.Annotations/Resources/Annotations/HasManyAttribute.netstandard.cs

+2
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ namespace JsonApiDotNetCore.Resources.Annotations;
99
[AttributeUsage(AttributeTargets.Property)]
1010
public sealed class HasManyAttribute : RelationshipAttribute
1111
{
12+
/// <summary />
13+
public HasManyCapabilities Capabilities { get; set; }
1214
}

0 commit comments

Comments
 (0)