Skip to content

Commit 560f19e

Browse files
authored
Merge pull request #69 from Research-Institute/feat/sparse-field-sets
Feat/sparse field sets
2 parents 2b80385 + 0179f9e commit 560f19e

File tree

15 files changed

+267
-37
lines changed

15 files changed

+267
-37
lines changed

README.md

+18
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ JsonApiDotnetCore provides a framework for building [json:api](http://jsonapi.or
2828
- [Meta](#meta)
2929
- [Client Generated Ids](#client-generated-ids)
3030
- [Custom Errors](#custom-errors)
31+
- [Sparse Fieldsets](#sparse-fieldsets)
3132
- [Tests](#tests)
3233

3334
## Comprehensive Demo
@@ -397,6 +398,23 @@ public override async Task<IActionResult> PostAsync([FromBody] MyEntity entity)
397398
}
398399
```
399400

401+
### Sparse Fieldsets
402+
403+
We currently support top-level field selection.
404+
What this means is you can restrict which fields are returned by a query using the `fields` query parameter, but this does not yet apply to included relationships.
405+
406+
- Currently valid:
407+
```http
408+
GET /articles?fields[articles]=title,body HTTP/1.1
409+
Accept: application/vnd.api+json
410+
```
411+
412+
- Not yet supported:
413+
```http
414+
GET /articles?include=author&fields[articles]=title,body&fields[people]=name HTTP/1.1
415+
Accept: application/vnd.api+json
416+
```
417+
400418
## Tests
401419

402420
I am using DotNetCoreDocs to generate sample requests and documentation.

src/JsonApiDotNetCore/Builders/DocumentBuilder.cs

+35-27
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@ public Document Build(IIdentifiable entity)
3333

3434
var document = new Document
3535
{
36-
Data = _getData(contextEntity, entity),
37-
Meta = _getMeta(entity),
36+
Data = GetData(contextEntity, entity),
37+
Meta = GetMeta(entity),
3838
Links = _jsonApiContext.PageManager.GetPageLinks(new LinkBuilder(_jsonApiContext))
3939
};
4040

41-
document.Included = _appendIncludedObject(document.Included, contextEntity, entity);
41+
document.Included = AppendIncludedObject(document.Included, contextEntity, entity);
4242

4343
return document;
4444
}
@@ -54,20 +54,20 @@ public Documents Build(IEnumerable<IIdentifiable> entities)
5454
var documents = new Documents
5555
{
5656
Data = new List<DocumentData>(),
57-
Meta = _getMeta(entities.FirstOrDefault()),
57+
Meta = GetMeta(entities.FirstOrDefault()),
5858
Links = _jsonApiContext.PageManager.GetPageLinks(new LinkBuilder(_jsonApiContext))
5959
};
6060

6161
foreach (var entity in entities)
6262
{
63-
documents.Data.Add(_getData(contextEntity, entity));
64-
documents.Included = _appendIncludedObject(documents.Included, contextEntity, entity);
63+
documents.Data.Add(GetData(contextEntity, entity));
64+
documents.Included = AppendIncludedObject(documents.Included, contextEntity, entity);
6565
}
6666

6767
return documents;
6868
}
6969

70-
private Dictionary<string, object> _getMeta(IIdentifiable entity)
70+
private Dictionary<string, object> GetMeta(IIdentifiable entity)
7171
{
7272
if (entity == null) return null;
7373

@@ -87,9 +87,9 @@ private Dictionary<string, object> _getMeta(IIdentifiable entity)
8787
return null;
8888
}
8989

90-
private List<DocumentData> _appendIncludedObject(List<DocumentData> includedObject, ContextEntity contextEntity, IIdentifiable entity)
90+
private List<DocumentData> AppendIncludedObject(List<DocumentData> includedObject, ContextEntity contextEntity, IIdentifiable entity)
9191
{
92-
var includedEntities = _getIncludedEntities(contextEntity, entity);
92+
var includedEntities = GetIncludedEntities(contextEntity, entity);
9393
if (includedEntities.Count > 0)
9494
{
9595
if (includedObject == null)
@@ -100,7 +100,7 @@ private List<DocumentData> _appendIncludedObject(List<DocumentData> includedObje
100100
return includedObject;
101101
}
102102

103-
private DocumentData _getData(ContextEntity contextEntity, IIdentifiable entity)
103+
private DocumentData GetData(ContextEntity contextEntity, IIdentifiable entity)
104104
{
105105
var data = new DocumentData
106106
{
@@ -115,16 +115,24 @@ private DocumentData _getData(ContextEntity contextEntity, IIdentifiable entity)
115115

116116
contextEntity.Attributes.ForEach(attr =>
117117
{
118-
data.Attributes.Add(attr.PublicAttributeName, attr.GetValue(entity));
118+
if(ShouldIncludeAttribute(attr))
119+
data.Attributes.Add(attr.PublicAttributeName, attr.GetValue(entity));
119120
});
120121

121122
if (contextEntity.Relationships.Count > 0)
122-
_addRelationships(data, contextEntity, entity);
123+
AddRelationships(data, contextEntity, entity);
123124

124125
return data;
125126
}
126127

127-
private void _addRelationships(DocumentData data, ContextEntity contextEntity, IIdentifiable entity)
128+
private bool ShouldIncludeAttribute(AttrAttribute attr)
129+
{
130+
return (_jsonApiContext.QuerySet == null
131+
|| _jsonApiContext.QuerySet.Fields.Count == 0
132+
|| _jsonApiContext.QuerySet.Fields.Contains(attr.InternalAttributeName));
133+
}
134+
135+
private void AddRelationships(DocumentData data, ContextEntity contextEntity, IIdentifiable entity)
128136
{
129137
data.Relationships = new Dictionary<string, RelationshipData>();
130138
var linkBuilder = new LinkBuilder(_jsonApiContext);
@@ -140,57 +148,57 @@ private void _addRelationships(DocumentData data, ContextEntity contextEntity, I
140148
}
141149
};
142150

143-
if (_relationshipIsIncluded(r.InternalRelationshipName))
151+
if (RelationshipIsIncluded(r.InternalRelationshipName))
144152
{
145153
var navigationEntity = _jsonApiContext.ContextGraph
146154
.GetRelationship(entity, r.InternalRelationshipName);
147155

148156
if(navigationEntity == null)
149157
relationshipData.SingleData = null;
150158
else if (navigationEntity is IEnumerable)
151-
relationshipData.ManyData = _getRelationships((IEnumerable<object>)navigationEntity, r.InternalRelationshipName);
159+
relationshipData.ManyData = GetRelationships((IEnumerable<object>)navigationEntity, r.InternalRelationshipName);
152160
else
153-
relationshipData.SingleData = _getRelationship(navigationEntity, r.InternalRelationshipName);
161+
relationshipData.SingleData = GetRelationship(navigationEntity, r.InternalRelationshipName);
154162
}
155163

156164
data.Relationships.Add(r.InternalRelationshipName.Dasherize(), relationshipData);
157165
});
158166
}
159167

160-
private List<DocumentData> _getIncludedEntities(ContextEntity contextEntity, IIdentifiable entity)
168+
private List<DocumentData> GetIncludedEntities(ContextEntity contextEntity, IIdentifiable entity)
161169
{
162170
var included = new List<DocumentData>();
163171

164172
contextEntity.Relationships.ForEach(r =>
165173
{
166-
if (!_relationshipIsIncluded(r.InternalRelationshipName)) return;
174+
if (!RelationshipIsIncluded(r.InternalRelationshipName)) return;
167175

168176
var navigationEntity = _jsonApiContext.ContextGraph.GetRelationship(entity, r.InternalRelationshipName);
169177

170178
if (navigationEntity is IEnumerable)
171179
foreach (var includedEntity in (IEnumerable)navigationEntity)
172-
_addIncludedEntity(included, (IIdentifiable)includedEntity);
180+
AddIncludedEntity(included, (IIdentifiable)includedEntity);
173181
else
174-
_addIncludedEntity(included, (IIdentifiable)navigationEntity);
182+
AddIncludedEntity(included, (IIdentifiable)navigationEntity);
175183
});
176184

177185
return included;
178186
}
179187

180-
private void _addIncludedEntity(List<DocumentData> entities, IIdentifiable entity)
188+
private void AddIncludedEntity(List<DocumentData> entities, IIdentifiable entity)
181189
{
182-
var includedEntity = _getIncludedEntity(entity);
190+
var includedEntity = GetIncludedEntity(entity);
183191
if(includedEntity != null)
184192
entities.Add(includedEntity);
185193
}
186194

187-
private DocumentData _getIncludedEntity(IIdentifiable entity)
195+
private DocumentData GetIncludedEntity(IIdentifiable entity)
188196
{
189197
if(entity == null) return null;
190198

191199
var contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(entity.GetType());
192200

193-
var data = _getData(contextEntity, entity);
201+
var data = GetData(contextEntity, entity);
194202

195203
data.Attributes = new Dictionary<string, object>();
196204

@@ -202,13 +210,13 @@ private DocumentData _getIncludedEntity(IIdentifiable entity)
202210
return data;
203211
}
204212

205-
private bool _relationshipIsIncluded(string relationshipName)
213+
private bool RelationshipIsIncluded(string relationshipName)
206214
{
207215
return _jsonApiContext.IncludedRelationships != null &&
208216
_jsonApiContext.IncludedRelationships.Contains(relationshipName.ToProperCase());
209217
}
210218

211-
private List<Dictionary<string, string>> _getRelationships(IEnumerable<object> entities, string relationshipName)
219+
private List<Dictionary<string, string>> GetRelationships(IEnumerable<object> entities, string relationshipName)
212220
{
213221
var objType = entities.GetType().GenericTypeArguments[0];
214222

@@ -224,7 +232,7 @@ private List<Dictionary<string, string>> _getRelationships(IEnumerable<object> e
224232
}
225233
return relationships;
226234
}
227-
private Dictionary<string, string> _getRelationship(object entity, string relationshipName)
235+
private Dictionary<string, string> GetRelationship(object entity, string relationshipName)
228236
{
229237
var objType = entity.GetType();
230238

src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public DefaultEntityRepository(
4848

4949
public virtual IQueryable<TEntity> Get()
5050
{
51-
return _dbSet;
51+
return _dbSet.Select(_jsonApiContext.QuerySet?.Fields);
5252
}
5353

5454
public virtual IQueryable<TEntity> Filter(IQueryable<TEntity> entities, FilterQuery filterQuery)
@@ -76,12 +76,12 @@ public virtual IQueryable<TEntity> Sort(IQueryable<TEntity> entities, List<SortQ
7676

7777
public virtual async Task<TEntity> GetAsync(TId id)
7878
{
79-
return await _dbSet.SingleOrDefaultAsync(e => e.Id.Equals(id));
79+
return await Get().SingleOrDefaultAsync(e => e.Id.Equals(id));
8080
}
8181

8282
public virtual async Task<TEntity> GetAndIncludeAsync(TId id, string relationshipName)
8383
{
84-
return await _dbSet
84+
return await Get()
8585
.Include(relationshipName)
8686
.SingleOrDefaultAsync(e => e.Id.Equals(id));
8787
}

src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs

+26-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
21
using System;
2+
using System.Collections.Generic;
33
using System.Linq;
44
using System.Linq.Expressions;
55
using System.Reflection;
@@ -126,5 +126,30 @@ public static IQueryable<TSource> Filter<TSource>(this IQueryable<TSource> sourc
126126
throw new JsonApiException("400", $"Could not cast {filterQuery.PropertyValue} to {property.PropertyType.Name}");
127127
}
128128
}
129+
public static IQueryable<TSource> Select<TSource>(this IQueryable<TSource> source, IEnumerable<string> columns)
130+
{
131+
if(columns == null || columns.Count() == 0)
132+
return source;
133+
134+
var sourceType = source.ElementType;
135+
136+
var resultType = typeof(TSource);
137+
138+
// {model}
139+
var parameter = Expression.Parameter(sourceType, "model");
140+
141+
var bindings = columns.Select(column => Expression.Bind(
142+
resultType.GetProperty(column), Expression.PropertyOrField(parameter, column)));
143+
144+
// { new Model () { Property = model.Property } }
145+
var body = Expression.MemberInit(Expression.New(resultType), bindings);
146+
147+
// { model => new TodoItem() { Property = model.Property } }
148+
var selector = Expression.Lambda(body, parameter);
149+
150+
return source.Provider.CreateQuery<TSource>(
151+
Expression.Call(typeof(Queryable), "Select", new Type[] { sourceType, resultType },
152+
source.Expression, Expression.Quote(selector)));
153+
}
129154
}
130155
}

src/JsonApiDotNetCore/Internal/Query/QuerySet.cs

+31
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ public QuerySet(
1919
_jsonApiContext = jsonApiContext;
2020
PageQuery = new PageQuery();
2121
Filters = new List<FilterQuery>();
22+
Fields = new List<string>();
2223
BuildQuerySet(query);
2324
}
2425

2526
public List<FilterQuery> Filters { get; set; }
2627
public PageQuery PageQuery { get; set; }
2728
public List<SortQuery> SortParameters { get; set; }
2829
public List<string> IncludedRelationships { get; set; }
30+
public List<string> Fields { get; set; }
2931

3032
private void BuildQuerySet(IQueryCollection query)
3133
{
@@ -55,6 +57,12 @@ private void BuildQuerySet(IQueryCollection query)
5557
continue;
5658
}
5759

60+
if (pair.Key.StartsWith("fields"))
61+
{
62+
Fields = ParseFieldsQuery(pair.Key, pair.Value);
63+
continue;
64+
}
65+
5866
throw new JsonApiException("400", $"{pair} is not a valid query.");
5967
}
6068
}
@@ -160,6 +168,29 @@ private List<string> ParseIncludedRelationships(string value)
160168
.ToList();
161169
}
162170

171+
private List<string> ParseFieldsQuery(string key, string value)
172+
{
173+
// expected: fields[TYPE]=prop1,prop2
174+
var typeName = key.Split('[', ']')[1];
175+
176+
var includedFields = new List<string> { "Id" };
177+
178+
if(typeName != _jsonApiContext.RequestEntity.EntityName.Dasherize())
179+
return includedFields;
180+
181+
var fields = value.Split(',');
182+
foreach(var field in fields)
183+
{
184+
var internalAttrName = _jsonApiContext.RequestEntity
185+
.Attributes
186+
.SingleOrDefault(attr => attr.PublicAttributeName == field)
187+
.InternalAttributeName;
188+
includedFields.Add(internalAttrName);
189+
}
190+
191+
return includedFields;
192+
}
193+
163194
private AttrAttribute GetAttribute(string propertyName)
164195
{
165196
return _jsonApiContext.RequestEntity.Attributes

test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorTests.cs

-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
1-
using DotNetCoreDocs;
2-
using JsonApiDotNetCoreExample;
3-
using DotNetCoreDocs.Writers;
41
using Newtonsoft.Json;
52
using JsonApiDotNetCore.Internal;
63
using JsonApiDotNetCore.Serialization;
74
using Xunit;
8-
using System.Diagnostics;
95

106
namespace JsonApiDotNetCoreExampleTests.Acceptance.Extensibility
117
{
@@ -14,8 +10,6 @@ public class CustomErrorTests
1410
[Fact]
1511
public void Can_Return_Custom_Error_Types()
1612
{
17-
// while(!Debugger.IsAttached) { bool stop = false; }
18-
1913
// arrange
2014
var error = new CustomError("507", "title", "detail", "custom");
2115
var errorCollection = new ErrorCollection();

0 commit comments

Comments
 (0)