Skip to content

Relationship capabilities and bugfixes #1197

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 8 commits into from
Oct 20, 2022
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
4 changes: 2 additions & 2 deletions docs/usage/extensibility/resource-definitions.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ from Entity Framework Core `IQueryable` execution.

### Excluding fields

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

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

```c#
public class UserDefinition : JsonApiResourceDefinition<User, int>
Expand Down
43 changes: 29 additions & 14 deletions docs/usage/resources/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@ options.DefaultAttrCapabilities = AttrCapabilities.None; // default: All

This can be overridden per attribute.

### Viewability
### AllowView

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.
Indicates whether the attribute value can be returned in responses. When not allowed and requested using `?fields[]=`, it results in an HTTP 400 response.
Otherwise, the attribute is silently omitted.

```c#
#nullable enable
Expand All @@ -57,45 +58,59 @@ public class User : Identifiable<int>
}
```

### Creatability
### AllowFilter

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.
Indicates whether the attribute can be filtered on. When not allowed and used in `?filter=`, an HTTP 400 is returned.

```c#
#nullable enable

public class Person : Identifiable<int>
{
[Attr(Capabilities = AttrCapabilities.AllowCreate)]
public string? CreatorName { get; set; }
[Attr(Capabilities = AttrCapabilities.AllowFilter)]
public string? FirstName { get; set; }
}
```

### Changeability
### AllowSort

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.
Indicates whether the attribute can be sorted on. When not allowed and used in `?sort=`, an HTTP 400 is returned.

```c#
#nullable enable

public class Person : Identifiable<int>
{
[Attr(Capabilities = AttrCapabilities.AllowChange)]
public string? FirstName { get; set; };
[Attr(Capabilities = ~AttrCapabilities.AllowSort)]
public string? FirstName { get; set; }
}
```

### Filter/Sort-ability
### AllowCreate

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

```c#
#nullable enable

public class Person : Identifiable<int>
{
[Attr(Capabilities = AttrCapabilities.AllowSort | AttrCapabilities.AllowFilter)]
public string? FirstName { get; set; }
[Attr(Capabilities = AttrCapabilities.AllowCreate)]
public string? CreatorName { get; set; }
}
```

### AllowChange

Indicates whether PATCH requests can update the attribute value. When sent but not allowed, an HTTP 422 response is returned.

```c#
#nullable enable

public class Person : Identifiable<int>
{
[Attr(Capabilities = AttrCapabilities.AllowChange)]
public string? FirstName { get; set; };
}
```

Expand Down
106 changes: 105 additions & 1 deletion docs/usage/resources/relationships.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,111 @@ public class TodoItem : Identifiable<int>
}
```

## Includibility
## Capabilities

_since v5.1_

Default JSON:API relationship capabilities are specified in
@JsonApiDotNetCore.Configuration.JsonApiOptions#JsonApiDotNetCore_Configuration_JsonApiOptions_DefaultHasOneCapabilities and
@JsonApiDotNetCore.Configuration.JsonApiOptions#JsonApiDotNetCore_Configuration_JsonApiOptions_DefaultHasManyCapabilities:

```c#
options.DefaultHasOneCapabilities = HasOneCapabilities.None; // default: All
options.DefaultHasManyCapabilities = HasManyCapabilities.None; // default: All
```

This can be overridden per relationship.

### AllowView

Indicates whether the relationship can be returned in responses. When not allowed and requested using `?fields[]=`, it results in an HTTP 400 response.
Otherwise, the relationship (and its related resources, when included) are silently omitted.

Note that this setting does not affect retrieving the related resources directly.

```c#
#nullable enable

public class User : Identifiable<int>
{
[HasOne(Capabilities = ~HasOneCapabilities.AllowView)]
public LoginAccount Account { get; set; } = null!;
}
```

### AllowInclude

Indicates whether the relationship can be included. When not allowed and used in `?include=`, an HTTP 400 is returned.

```c#
#nullable enable

public class User : Identifiable<int>
{
[HasMany(Capabilities = ~HasManyCapabilities.AllowInclude)]
public ISet<Group> Groups { get; set; } = new HashSet<Group>();
}
```

### AllowFilter

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.

```c#
#nullable enable

public class User : Identifiable<int>
{
[HasMany(Capabilities = HasManyCapabilities.AllowFilter)]
public ISet<Group> Groups { get; set; } = new HashSet<Group>();
}
```

### AllowSet

Indicates whether POST and PATCH requests can replace the relationship. When sent but not allowed, an HTTP 422 response is returned.

```c#
#nullable enable

public class User : Identifiable<int>
{
[HasOne(Capabilities = ~HasOneCapabilities.AllowSet)]
public LoginAccount Account { get; set; } = null!;
}
```

### AllowAdd

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.

```c#
#nullable enable

public class User : Identifiable<int>
{
[HasMany(Capabilities = ~HasManyCapabilities.AllowAdd)]
public ISet<Group> Groups { get; set; } = new HashSet<Group>();
}
```

### AllowRemove

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.

```c#
#nullable enable

public class User : Identifiable<int>
{
[HasMany(Capabilities = ~HasManyCapabilities.AllowRemove)]
public ISet<Group> Groups { get; set; } = new HashSet<Group>();
}
```

## CanInclude

_obsolete since v5.1_

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

Expand Down
4 changes: 2 additions & 2 deletions src/Examples/JsonApiDotNetCoreExample/Models/TodoItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ public sealed class TodoItem : Identifiable<int>
[HasOne]
public Person Owner { get; set; } = null!;

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

[HasMany]
[HasMany(Capabilities = HasManyCapabilities.AllowView | HasManyCapabilities.AllowFilter)]
public ISet<Tag> Tags { get; set; } = new HashSet<Tag>();
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(TargetFrameworkName);netstandard1.0</TargetFrameworks>
<IsPackable>true</IsPackable>
Expand Down Expand Up @@ -45,4 +45,9 @@
<Compile Remove="**/*.netstandard.cs" />
<None Include="**/*.netstandard.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="SauceControl.InheritDoc" Version="1.3.0" PrivateAssets="All" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,16 @@ public sealed class AttrAttribute : ResourceFieldAttribute
internal bool HasExplicitCapabilities => _capabilities != null;

/// <summary>
/// The set of capabilities that are allowed to be performed on this attribute. When not explicitly assigned, the configured default set of capabilities
/// is used.
/// The set of allowed capabilities on this attribute. When not explicitly set, the configured default set of capabilities is used.
/// </summary>
/// <example>
/// <code>
/// public class Author : Identifiable
/// <code><![CDATA[
/// public class Author : Identifiable<long>
/// {
/// [Attr(Capabilities = AttrCapabilities.AllowFilter | AttrCapabilities.AllowSort)]
/// public string Name { get; set; }
/// public string Name { get; set; } = null!;
/// }
/// </code>
/// ]]></code>
/// </example>
public AttrCapabilities Capabilities
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace JsonApiDotNetCore.Resources.Annotations;

/// <summary>
/// Indicates capabilities that can be performed on an <see cref="AttrAttribute" />.
/// Indicates what can be performed on an <see cref="AttrAttribute" />.
/// </summary>
[PublicAPI]
[Flags]
Expand All @@ -12,29 +12,32 @@ public enum AttrCapabilities
None = 0,

/// <summary>
/// Whether or not GET requests can retrieve the attribute. Attempts to retrieve when disabled will return an HTTP 400 response.
/// 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
/// disabled will return an HTTP 400 response. Otherwise, the attribute is silently omitted.
/// </summary>
AllowView = 1,

/// <summary>
/// Whether or not POST requests can assign the attribute value. Attempts to assign when disabled will return an HTTP 422 response.
/// </summary>
AllowCreate = 2,
AllowCreate = 1 << 1,

/// <summary>
/// Whether or not PATCH requests can update the attribute value. Attempts to update when disabled will return an HTTP 422 response.
/// </summary>
AllowChange = 4,
AllowChange = 1 << 2,

/// <summary>
/// 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.
/// 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
/// response.
/// </summary>
AllowFilter = 8,
AllowFilter = 1 << 3,

/// <summary>
/// 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.
/// 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
/// response.
/// </summary>
AllowSort = 16,
AllowSort = 1 << 4,

All = AllowView | AllowCreate | AllowChange | AllowFilter | AllowSort
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using JetBrains.Annotations;

// ReSharper disable NonReadonlyMemberInGetHashCode

namespace JsonApiDotNetCore.Resources.Annotations;

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

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

internal bool HasExplicitCapabilities => _capabilities != null;

/// <summary>
/// The set of allowed capabilities on this to-many relationship. When not explicitly set, the configured default set of capabilities is used.
/// </summary>
/// <example>
/// <code><![CDATA[
/// public class Book : Identifiable<long>
/// {
/// [HasMany(Capabilities = HasManyCapabilities.AllowView | HasManyCapabilities.AllowInclude)]
/// public ISet<Chapter> Chapters { get; set; } = new HashSet<Chapter>();
/// }
/// ]]></code>
/// </example>
public HasManyCapabilities Capabilities
{
get => _capabilities ?? default;
set => _capabilities = value;
}

public HasManyAttribute()
{
_lazyIsManyToMany = new Lazy<bool>(EvaluateIsManyToMany, LazyThreadSafetyMode.PublicationOnly);
Expand All @@ -41,4 +64,26 @@ private bool EvaluateIsManyToMany()

return false;
}

public override bool Equals(object? obj)
{
if (ReferenceEquals(this, obj))
{
return true;
}

if (obj is null || GetType() != obj.GetType())
{
return false;
}

var other = (HasManyAttribute)obj;

return _capabilities == other._capabilities && base.Equals(other);
}

public override int GetHashCode()
{
return HashCode.Combine(_capabilities, base.GetHashCode());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ namespace JsonApiDotNetCore.Resources.Annotations;
[AttributeUsage(AttributeTargets.Property)]
public sealed class HasManyAttribute : RelationshipAttribute
{
/// <summary />
public HasManyCapabilities Capabilities { get; set; }
}
Loading