Skip to content

Commit b02445c

Browse files
committed
fix: #583
1 parent a94f318 commit b02445c

File tree

3 files changed

+109
-28
lines changed

3 files changed

+109
-28
lines changed

src/JsonApiDotNetCore/QueryParameterServices/SparseFieldsService.cs

Lines changed: 52 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -40,40 +40,66 @@ public List<AttrAttribute> Get(RelationshipAttribute relationship = null)
4040
return fields;
4141
}
4242

43+
4344
/// <inheritdoc/>
4445
public virtual void Parse(KeyValuePair<string, StringValues> queryParameter)
45-
{
46-
// expected: fields[TYPE]=prop1,prop2
47-
var typeName = queryParameter.Key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1];
46+
{ // expected: articles?fields=prop1,prop2
47+
// articles?fields[articles]=prop1,prop2
48+
// articles?fields[relationship]=prop1,prop2
4849
var fields = new List<string> { nameof(Identifiable.Id) };
50+
fields.AddRange(((string)queryParameter.Value).Split(QueryConstants.COMMA));
4951

50-
var relationship = _requestResource.Relationships.SingleOrDefault(a => a.Is(typeName));
51-
if (relationship == null && string.Equals(typeName, _requestResource.EntityName, StringComparison.OrdinalIgnoreCase) == false)
52-
throw new JsonApiException(400, $"fields[{typeName}] is invalid");
52+
var keySplitted = queryParameter.Key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET);
5353

54-
fields.AddRange(((string)queryParameter.Value).Split(QueryConstants.COMMA));
55-
foreach (var field in fields)
56-
{
57-
if (relationship != default)
58-
{
59-
var relationProperty = _contextEntityProvider.GetContextEntity(relationship.DependentType);
60-
var attr = relationProperty.Attributes.SingleOrDefault(a => a.Is(field));
61-
if (attr == null)
62-
throw new JsonApiException(400, $"'{relationship.DependentType.Name}' does not contain '{field}'.");
54+
if (keySplitted.Count() == 1) // input format: fields=prop1,prop2
55+
foreach (var field in fields)
56+
RegisterRequestResourceField(field);
57+
else
58+
{ // input format: fields[articles]=prop1,prop2
59+
string navigation = keySplitted[1];
60+
// it is possible that the request resource has a relationship
61+
// that is equal to the resource name, like with self-referering data types (eg directory structures)
62+
// if not, no longer support this type of sparse field selection.
63+
if (navigation == _requestResource.EntityName && !_requestResource.Relationships.Any(a => a.Is(navigation)))
64+
throw new JsonApiException(400, $"Use \"?fields=...\" instead of \"fields[{navigation}]\":" +
65+
$" the square bracket navigations is now reserved " +
66+
$"for relationships only. See https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/555#issuecomment-543100865");
67+
68+
var relationship = _requestResource.Relationships.SingleOrDefault(a => a.Is(navigation));
69+
if (relationship == null)
70+
throw new JsonApiException(400, $"\"{navigation}\" in \"fields[{navigation}]\" is not a valid relationship of {_requestResource.EntityName}");
6371

64-
if (!_selectedRelationshipFields.TryGetValue(relationship, out var registeredFields))
65-
_selectedRelationshipFields.Add(relationship, registeredFields = new List<AttrAttribute>());
66-
registeredFields.Add(attr);
67-
}
68-
else
69-
{
70-
var attr = _requestResource.Attributes.SingleOrDefault(a => a.Is(field));
71-
if (attr == null)
72-
throw new JsonApiException(400, $"'{_requestResource.EntityName}' does not contain '{field}'.");
72+
foreach (var field in fields)
73+
RegisterRelatedResourceField(field, relationship);
7374

74-
(_selectedFields = _selectedFields ?? new List<AttrAttribute>()).Add(attr);
75-
}
7675
}
7776
}
77+
78+
/// <summary>
79+
/// Registers field selection queries of the form articles?fields[author]=first-name
80+
/// </summary>
81+
private void RegisterRelatedResourceField(string field, RelationshipAttribute relationship)
82+
{
83+
var relationProperty = _contextEntityProvider.GetContextEntity(relationship.DependentType);
84+
var attr = relationProperty.Attributes.SingleOrDefault(a => a.Is(field));
85+
if (attr == null)
86+
throw new JsonApiException(400, $"'{relationship.DependentType.Name}' does not contain '{field}'.");
87+
88+
if (!_selectedRelationshipFields.TryGetValue(relationship, out var registeredFields))
89+
_selectedRelationshipFields.Add(relationship, registeredFields = new List<AttrAttribute>());
90+
registeredFields.Add(attr);
91+
}
92+
93+
/// <summary>
94+
/// Registers field selection queries of the form articles?fields=title
95+
/// </summary>
96+
private void RegisterRequestResourceField(string field)
97+
{
98+
var attr = _requestResource.Attributes.SingleOrDefault(a => a.Is(field));
99+
if (attr == null)
100+
throw new JsonApiException(400, $"'{_requestResource.EntityName}' does not contain '{field}'.");
101+
102+
(_selectedFields = _selectedFields ?? new List<AttrAttribute>()).Add(attr);
103+
}
78104
}
79105
}

test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ public async Task Fields_Query_Selects_Sparse_Field_Sets()
104104
var server = new TestServer(builder);
105105
var client = server.CreateClient();
106106

107-
var route = $"/api/v1/todo-items/{todoItem.Id}?fields[todo-items]=description,created-date";
107+
var route = $"/api/v1/todo-items/{todoItem.Id}?fields=description,created-date";
108108
var request = new HttpRequestMessage(httpMethod, route);
109109

110110
// act
@@ -119,6 +119,36 @@ public async Task Fields_Query_Selects_Sparse_Field_Sets()
119119
Assert.Equal(todoItem.CreatedDate.ToString("G"), ((DateTime)deserializeBody.SingleData.Attributes["created-date"]).ToString("G"));
120120
}
121121

122+
[Fact]
123+
public async Task Fields_Query_Selects_Sparse_Field_Sets_With_Type_As_Navigation()
124+
{
125+
// arrange
126+
var todoItem = new TodoItem
127+
{
128+
Description = "description",
129+
Ordinal = 1,
130+
CreatedDate = DateTime.Now
131+
};
132+
_dbContext.TodoItems.Add(todoItem);
133+
await _dbContext.SaveChangesAsync();
134+
135+
var builder = new WebHostBuilder()
136+
.UseStartup<Startup>();
137+
var httpMethod = new HttpMethod("GET");
138+
var server = new TestServer(builder);
139+
var client = server.CreateClient();
140+
var route = $"/api/v1/todo-items/{todoItem.Id}?fields[todo-items]=description,created-date";
141+
var request = new HttpRequestMessage(httpMethod, route);
142+
143+
// act
144+
var response = await client.SendAsync(request);
145+
var body = await response.Content.ReadAsStringAsync();
146+
147+
// assert
148+
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
149+
Assert.Contains("relationships only", body);
150+
}
151+
122152
[Fact]
123153
public async Task Fields_Query_Selects_All_Fieldset_With_HasOne()
124154
{

test/UnitTests/QueryParameters/SparseFieldsServiceTests.cs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public void Parse_ValidSelection_CanParse()
3838
var attribute = new AttrAttribute(attrName) { InternalAttributeName = internalAttrName };
3939
var idAttribute = new AttrAttribute("id") { InternalAttributeName = "Id" };
4040

41-
var query = new KeyValuePair<string, StringValues>($"fields[{type}]", new StringValues(attrName));
41+
var query = new KeyValuePair<string, StringValues>($"fields", new StringValues(attrName));
4242

4343
var contextEntity = new ContextEntity
4444
{
@@ -58,6 +58,31 @@ public void Parse_ValidSelection_CanParse()
5858
Assert.Equal(attribute, result[1]);
5959
}
6060

61+
[Fact]
62+
public void Parse_TypeNameAsNavigation_ThrowsJsonApiException()
63+
{
64+
// arrange
65+
const string type = "articles";
66+
const string attrName = "some-field";
67+
const string internalAttrName = "SomeField";
68+
var attribute = new AttrAttribute(attrName) { InternalAttributeName = internalAttrName };
69+
var idAttribute = new AttrAttribute("id") { InternalAttributeName = "Id" };
70+
71+
var query = new KeyValuePair<string, StringValues>($"fields[{type}]", new StringValues(attrName));
72+
73+
var contextEntity = new ContextEntity
74+
{
75+
EntityName = type,
76+
Attributes = new List<AttrAttribute> { attribute, idAttribute },
77+
Relationships = new List<RelationshipAttribute>()
78+
};
79+
var service = GetService(contextEntity);
80+
81+
// act, assert
82+
var ex = Assert.Throws<JsonApiException>(() => service.Parse(query));
83+
Assert.Contains("relationships only", ex.Message);
84+
}
85+
6186
[Fact]
6287
public void Parse_InvalidField_ThrowsJsonApiException()
6388
{

0 commit comments

Comments
 (0)