-
-
Notifications
You must be signed in to change notification settings - Fork 163
Expand file tree
/
Copy pathHasManyAttribute.cs
More file actions
153 lines (129 loc) · 4.91 KB
/
HasManyAttribute.cs
File metadata and controls
153 lines (129 loc) · 4.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
using System.Collections;
using JetBrains.Annotations;
// ReSharper disable NonReadonlyMemberInGetHashCode
namespace JsonApiDotNetCore.Resources.Annotations;
/// <summary>
/// Used to expose a property on a resource class as a JSON:API to-many relationship
/// (https://jsonapi.org/format/#document-resource-object-relationships).
/// </summary>
/// <example>
/// <code><![CDATA[
/// public class Author : Identifiable
/// {
/// [HasMany]
/// public ISet<Article> Articles { get; set; }
/// }
/// ]]></code>
/// </example>
[PublicAPI]
[AttributeUsage(AttributeTargets.Property)]
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;
}
/// <summary>
/// When set to <c>true</c>, overrules the default page size, the page size from a resource definition, and the
/// <c>
/// page[size]
/// </c>
/// query string parameter by forcibly turning off pagination on the related resources for this relationship.
/// </summary>
/// <remarks>
/// Caution: only use this when the number of related resources (along with their nested includes) is known to always be small.
/// </remarks>
public bool DisablePagination { get; set; }
public HasManyAttribute()
{
_lazyIsManyToMany = new Lazy<bool>(EvaluateIsManyToMany, LazyThreadSafetyMode.PublicationOnly);
}
private bool EvaluateIsManyToMany()
{
if (InverseNavigationProperty != null)
{
Type? elementType = CollectionConverter.Instance.FindCollectionElementType(InverseNavigationProperty.PropertyType);
return elementType != null;
}
return false;
}
/// <inheritdoc />
public override void SetValue(object resource, object? newValue)
{
ArgumentNullException.ThrowIfNull(newValue);
AssertIsIdentifiableCollection(newValue);
base.SetValue(resource, newValue);
}
private void AssertIsIdentifiableCollection(object newValue)
{
if (newValue is not IEnumerable enumerable)
{
throw new InvalidOperationException($"Resource of type '{newValue.GetType()}' must be a collection.");
}
foreach (object? element in enumerable)
{
if (element == null)
{
throw new InvalidOperationException("Resource collection must not contain null values.");
}
AssertIsIdentifiable(element);
}
}
/// <summary>
/// Adds a resource to this to-many relationship on the specified resource instance. Throws if the property is read-only or if the field does not belong
/// to the specified resource instance.
/// </summary>
public void AddValue(object resource, IIdentifiable resourceToAdd)
{
ArgumentNullException.ThrowIfNull(resource);
ArgumentNullException.ThrowIfNull(resourceToAdd);
object? rightValue = GetValue(resource);
List<IIdentifiable> rightResources = CollectionConverter.Instance.ExtractResources(rightValue).ToList();
if (!rightResources.Exists(nextResource => nextResource == resourceToAdd))
{
rightResources.Add(resourceToAdd);
Type collectionType = rightValue?.GetType() ?? Property.PropertyType;
IEnumerable typedCollection = CollectionConverter.Instance.CopyToTypedCollection(rightResources, collectionType);
base.SetValue(resource, typedCollection);
}
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
public override int GetHashCode()
{
return HashCode.Combine(_capabilities, base.GetHashCode());
}
}