Skip to content

Commit 78137c1

Browse files
committed
Refactor FieldContainer to allow queries on subset of field chain
1 parent 8558a0e commit 78137c1

File tree

37 files changed

+381
-285
lines changed

37 files changed

+381
-285
lines changed

src/Examples/GettingStarted/GettingStarted.http

Lines changed: 87 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,39 @@ GET http://{{hostname}}:{{port}}/api/people
99

1010
GET http://{{hostname}}:{{port}}/api/people?include=books
1111

12-
### Filter inside compound attribute
12+
### Filter inside compound attribute chain
1313

14-
GET http://{{hostname}}:{{port}}/api/people?filter=startsWith(livingAddress.country,'T')
14+
GET http://{{hostname}}:{{port}}/api/people?filter=startsWith(livingAddress.country.code,'N')
1515

16-
### TODO: Test filters descending into compound attribute (requires an expression that has child scope: "has").
17-
### TODO: Consider supporting to-many filters on collections: "has", "any", "count"
18-
### Requires to act on parent Attr instead of ResourceType, which likely requires major breaking change.
16+
### Filter on collection attribute with "count" function
1917

18+
GET http://{{hostname}}:{{port}}/api/people?filter=greaterThan(count(namesOfChildren),'2')
19+
20+
### Filter on collection attribute with "has" function
21+
22+
GET http://{{hostname}}:{{port}}/api/people?filter=not(has(addresses))
23+
24+
### Filter on collection attribute with "has" function, taking nested filter
25+
26+
GET http://{{hostname}}:{{port}}/api/people?filter=has(addresses,equals(country.code,'ESP'))
2027

2128
### Patch string collection attribute
2229

2330
PATCH http://{{hostname}}:{{port}}/api/people/1
2431
Content-Type: application/vnd.api+json
2532

2633
{
27-
"data": {
28-
"type": "people",
29-
"id": "1",
30-
"attributes": {
31-
"namesOfChildren": ["Mary", "Ann", null]
32-
}
34+
"data": {
35+
"type": "people",
36+
"id": "1",
37+
"attributes": {
38+
"namesOfChildren": [
39+
"Mary",
40+
"Ann",
41+
null
42+
]
3343
}
44+
}
3445
}
3546

3647
### Patch int collection attribute
@@ -39,13 +50,17 @@ PATCH http://{{hostname}}:{{port}}/api/people/1
3950
Content-Type: application/vnd.api+json
4051

4152
{
42-
"data": {
43-
"type": "people",
44-
"id": "1",
45-
"attributes": {
46-
"agesOfChildren": [15, 25, null]
47-
}
53+
"data": {
54+
"type": "people",
55+
"id": "1",
56+
"attributes": {
57+
"agesOfChildren": [
58+
15,
59+
25,
60+
null
61+
]
4862
}
63+
}
4964
}
5065

5166
### Patch members of compound attribute
@@ -54,16 +69,19 @@ PATCH http://{{hostname}}:{{port}}/api/people/1
5469
Content-Type: application/vnd.api+json
5570

5671
{
57-
"data": {
58-
"type": "people",
59-
"id": "1",
60-
"attributes": {
61-
"livingAddress": {
62-
"country": "Germany",
63-
"street": "OtherStreet"
64-
}
65-
}
72+
"data": {
73+
"type": "people",
74+
"id": "1",
75+
"attributes": {
76+
"livingAddress": {
77+
"country": {
78+
"code": "ITA",
79+
"displayName": "Italy"
80+
},
81+
"street": "OtherStreet"
82+
}
6683
}
84+
}
6785
}
6886

6987
### Patch members of compound attribute, setting them to null
@@ -72,16 +90,16 @@ PATCH http://{{hostname}}:{{port}}/api/people/1
7290
Content-Type: application/vnd.api+json
7391

7492
{
75-
"data": {
76-
"type": "people",
77-
"id": "1",
78-
"attributes": {
79-
"livingAddress": {
80-
"country": null,
81-
"street": null
82-
}
83-
}
93+
"data": {
94+
"type": "people",
95+
"id": "1",
96+
"attributes": {
97+
"livingAddress": {
98+
"country": null,
99+
"street": null
100+
}
84101
}
102+
}
85103
}
86104

87105
### Patch compound attribute to empty object (must be a no-op)
@@ -90,13 +108,13 @@ PATCH http://{{hostname}}:{{port}}/api/people/1
90108
Content-Type: application/vnd.api+json
91109

92110
{
93-
"data": {
94-
"type": "people",
95-
"id": "1",
96-
"attributes": {
97-
"mailAddress": {}
98-
}
111+
"data": {
112+
"type": "people",
113+
"id": "1",
114+
"attributes": {
115+
"mailAddress": {}
99116
}
117+
}
100118
}
101119

102120
### Patch compound attribute to null
@@ -105,13 +123,13 @@ PATCH http://{{hostname}}:{{port}}/api/people/1
105123
Content-Type: application/vnd.api+json
106124

107125
{
108-
"data": {
109-
"type": "people",
110-
"id": "1",
111-
"attributes": {
112-
"mailAddress": null
113-
}
126+
"data": {
127+
"type": "people",
128+
"id": "1",
129+
"attributes": {
130+
"mailAddress": null
114131
}
132+
}
115133
}
116134

117135
### Patch compound nullable collection attribute (null element is blocked by EF Core)
@@ -120,19 +138,24 @@ PATCH http://{{hostname}}:{{port}}/api/people/1
120138
Content-Type: application/vnd.api+json
121139

122140
{
123-
"data": {
124-
"type": "people",
125-
"id": "1",
126-
"attributes": {
127-
"addresses": [{
128-
"country": "Germany"
129-
}, {
130-
"street": "SomeStreet"
131-
}, {
132-
}
133-
]
134-
}
141+
"data": {
142+
"type": "people",
143+
"id": "1",
144+
"attributes": {
145+
"addresses": [
146+
{
147+
"country": {
148+
"code": "DEU",
149+
"displayName": "Germany"
150+
}
151+
},
152+
{
153+
"street": "SomeStreet"
154+
},
155+
{}
156+
]
135157
}
158+
}
136159
}
137160

138161
### Patch relationships to null - SHOULD THIS FAIL OR NO-OP?
@@ -141,9 +164,9 @@ PATCH http://{{hostname}}:{{port}}/api/people/1
141164
Content-Type: application/vnd.api+json
142165

143166
{
144-
"data": {
145-
"type": "people",
146-
"id": "1",
147-
"relationships": null
148-
}
167+
"data": {
168+
"type": "people",
169+
"id": "1",
170+
"relationships": null
171+
}
149172
}

src/Examples/GettingStarted/Models/Address.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ public sealed class Address
1212
[Attr]
1313
public string? PostalCode { get; set; }
1414

15-
[Attr]
16-
public string? Country { get; set; }
15+
[Attr(IsCompound = true)]
16+
public Country? Country { get; set; }
1717

1818
public string? NotExposed { get; set; }
1919
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using JsonApiDotNetCore.Resources.Annotations;
2+
using Microsoft.EntityFrameworkCore;
3+
4+
namespace GettingStarted.Models;
5+
6+
[Owned]
7+
public sealed class Country
8+
{
9+
[Attr]
10+
public required string Code { get; set; }
11+
12+
[Attr]
13+
public string? DisplayName { get; set; }
14+
}

src/Examples/GettingStarted/Program.cs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
using Microsoft.EntityFrameworkCore;
66
using Microsoft.EntityFrameworkCore.Diagnostics;
77

8+
#pragma warning disable format
9+
810
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
911

1012
// Add services to the container.
@@ -74,14 +76,22 @@ static async Task CreateSampleDataAsync(SampleDbContext dbContext)
7476
{
7577
Street = "SomeStreet",
7678
PostalCode = "1234 AB",
77-
Country = "The Netherlands",
79+
Country = new Country
80+
{
81+
Code = "NLD",
82+
DisplayName = "The Netherlands"
83+
},
7884
NotExposed = "NotExposed"
7985
},
8086
MailAddress = new Address
8187
{
8288
Street = "MailStreet",
8389
PostalCode = "MailPostalCode",
84-
Country = "MailCountry",
90+
Country = new Country
91+
{
92+
Code = "MailCode",
93+
DisplayName = "MailCountryName"
94+
},
8595
NotExposed = "MailNotExposed"
8696
},
8797
Addresses =
@@ -90,14 +100,22 @@ static async Task CreateSampleDataAsync(SampleDbContext dbContext)
90100
{
91101
Street = "Street1",
92102
PostalCode = "PostalCode1",
93-
Country = "Country1",
103+
Country = new Country
104+
{
105+
Code = "ESP",
106+
DisplayName = "Spain"
107+
},
94108
NotExposed = "NotExposed1"
95109
},
96110
new Address
97111
{
98112
Street = "Street2",
99113
PostalCode = "PostalCode2",
100-
Country = "Country2",
114+
Country = new Country
115+
{
116+
Code = "Country2",
117+
DisplayName = "CountryName2"
118+
},
101119
NotExposed = "NotExposed2"
102120
}
103121
],
@@ -135,6 +153,7 @@ static async Task CreateSampleDataAsync(SampleDbContext dbContext)
135153
}
136154
}*/
137155
);
156+
#pragma warning restore format
138157

139158
await dbContext.SaveChangesAsync();
140159
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using JsonApiDotNetCore.Resources.Annotations;
2+
3+
namespace JsonApiDotNetCore.Configuration;
4+
5+
/// <summary>
6+
/// The containing type for a JSON:API field, which can be a <see cref="ResourceType" /> or an <see cref="AttrAttribute" />.
7+
/// </summary>
8+
public interface IFieldContainer
9+
{
10+
/// <summary>
11+
/// The publicly exposed name of this container.
12+
/// </summary>
13+
string PublicName { get; }
14+
15+
/// <summary>
16+
/// The CLR type of this container.
17+
/// </summary>
18+
Type ClrType { get; }
19+
20+
/// <summary>
21+
/// Searches the direct children of this container for an attribute with the specified name.
22+
/// </summary>
23+
/// <param name="publicName">
24+
/// The publicly exposed name of the attribute to find.
25+
/// </param>
26+
/// <returns>
27+
/// The attribute, or <c>null</c> if not found.
28+
/// </returns>
29+
AttrAttribute? FindAttributeByPublicName(string publicName);
30+
}

src/JsonApiDotNetCore.Annotations/Configuration/ResourceType.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace JsonApiDotNetCore.Configuration;
77
/// Metadata about the shape of a JSON:API resource in the resource graph.
88
/// </summary>
99
[PublicAPI]
10-
public sealed class ResourceType
10+
public sealed class ResourceType : IFieldContainer
1111
{
1212
private static readonly IReadOnlySet<ResourceType> EmptyResourceTypeSet = new HashSet<ResourceType>().AsReadOnly();
1313
private static readonly IReadOnlySet<AttrAttribute> EmptyAttributeSet = new HashSet<AttrAttribute>().AsReadOnly();

src/JsonApiDotNetCore.Annotations/Resources/Annotations/AttrAttribute.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections.ObjectModel;
22
using JetBrains.Annotations;
3+
using JsonApiDotNetCore.Configuration;
34

45
namespace JsonApiDotNetCore.Resources.Annotations;
56

@@ -8,7 +9,7 @@ namespace JsonApiDotNetCore.Resources.Annotations;
89
/// </summary>
910
[PublicAPI]
1011
[AttributeUsage(AttributeTargets.Property)]
11-
public sealed class AttrAttribute : ResourceFieldAttribute
12+
public sealed class AttrAttribute : ResourceFieldAttribute, IFieldContainer
1213
{
1314
private static readonly ReadOnlyDictionary<string, AttrAttribute> EmptyChildren = new Dictionary<string, AttrAttribute>().AsReadOnly();
1415

@@ -49,6 +50,15 @@ public AttrCapabilities Capabilities
4950
/// </summary>
5051
public IReadOnlyDictionary<string, AttrAttribute> Children { get; internal set; } = EmptyChildren;
5152

53+
/// <inheritdoc />
54+
public Type ClrType => Property.PropertyType;
55+
56+
/// <inheritdoc />
57+
public AttrAttribute? FindAttributeByPublicName(string publicName)
58+
{
59+
return Children.GetValueOrDefault(publicName);
60+
}
61+
5262
/// <inheritdoc />
5363
public override bool Equals(object? obj)
5464
{

0 commit comments

Comments
 (0)