Skip to content

Feat/sparse field sets #69

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 14 commits into from
Mar 21, 2017
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
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ JsonApiDotnetCore provides a framework for building [json:api](http://jsonapi.or
- [Meta](#meta)
- [Client Generated Ids](#client-generated-ids)
- [Custom Errors](#custom-errors)
- [Sparse Fieldsets](#sparse-fieldsets)
- [Tests](#tests)

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

### Sparse Fieldsets

We currently support top-level field selection.
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.

- Currently valid:
```http
GET /articles?fields[articles]=title,body HTTP/1.1
Accept: application/vnd.api+json
```

- Not yet supported:
```http
GET /articles?include=author&fields[articles]=title,body&fields[people]=name HTTP/1.1
Accept: application/vnd.api+json
```

## Tests

I am using DotNetCoreDocs to generate sample requests and documentation.
Expand Down
62 changes: 35 additions & 27 deletions src/JsonApiDotNetCore/Builders/DocumentBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ public Document Build(IIdentifiable entity)

var document = new Document
{
Data = _getData(contextEntity, entity),
Meta = _getMeta(entity),
Data = GetData(contextEntity, entity),
Meta = GetMeta(entity),
Links = _jsonApiContext.PageManager.GetPageLinks(new LinkBuilder(_jsonApiContext))
};

document.Included = _appendIncludedObject(document.Included, contextEntity, entity);
document.Included = AppendIncludedObject(document.Included, contextEntity, entity);

return document;
}
Expand All @@ -54,20 +54,20 @@ public Documents Build(IEnumerable<IIdentifiable> entities)
var documents = new Documents
{
Data = new List<DocumentData>(),
Meta = _getMeta(entities.FirstOrDefault()),
Meta = GetMeta(entities.FirstOrDefault()),
Links = _jsonApiContext.PageManager.GetPageLinks(new LinkBuilder(_jsonApiContext))
};

foreach (var entity in entities)
{
documents.Data.Add(_getData(contextEntity, entity));
documents.Included = _appendIncludedObject(documents.Included, contextEntity, entity);
documents.Data.Add(GetData(contextEntity, entity));
documents.Included = AppendIncludedObject(documents.Included, contextEntity, entity);
}

return documents;
}

private Dictionary<string, object> _getMeta(IIdentifiable entity)
private Dictionary<string, object> GetMeta(IIdentifiable entity)
{
if (entity == null) return null;

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

private List<DocumentData> _appendIncludedObject(List<DocumentData> includedObject, ContextEntity contextEntity, IIdentifiable entity)
private List<DocumentData> AppendIncludedObject(List<DocumentData> includedObject, ContextEntity contextEntity, IIdentifiable entity)
{
var includedEntities = _getIncludedEntities(contextEntity, entity);
var includedEntities = GetIncludedEntities(contextEntity, entity);
if (includedEntities.Count > 0)
{
if (includedObject == null)
Expand All @@ -100,7 +100,7 @@ private List<DocumentData> _appendIncludedObject(List<DocumentData> includedObje
return includedObject;
}

private DocumentData _getData(ContextEntity contextEntity, IIdentifiable entity)
private DocumentData GetData(ContextEntity contextEntity, IIdentifiable entity)
{
var data = new DocumentData
{
Expand All @@ -115,16 +115,24 @@ private DocumentData _getData(ContextEntity contextEntity, IIdentifiable entity)

contextEntity.Attributes.ForEach(attr =>
{
data.Attributes.Add(attr.PublicAttributeName, attr.GetValue(entity));
if(ShouldIncludeAttribute(attr))
data.Attributes.Add(attr.PublicAttributeName, attr.GetValue(entity));
});

if (contextEntity.Relationships.Count > 0)
_addRelationships(data, contextEntity, entity);
AddRelationships(data, contextEntity, entity);

return data;
}

private void _addRelationships(DocumentData data, ContextEntity contextEntity, IIdentifiable entity)
private bool ShouldIncludeAttribute(AttrAttribute attr)
{
return (_jsonApiContext.QuerySet == null
|| _jsonApiContext.QuerySet.Fields.Count == 0
|| _jsonApiContext.QuerySet.Fields.Contains(attr.InternalAttributeName));
}

private void AddRelationships(DocumentData data, ContextEntity contextEntity, IIdentifiable entity)
{
data.Relationships = new Dictionary<string, RelationshipData>();
var linkBuilder = new LinkBuilder(_jsonApiContext);
Expand All @@ -140,57 +148,57 @@ private void _addRelationships(DocumentData data, ContextEntity contextEntity, I
}
};

if (_relationshipIsIncluded(r.InternalRelationshipName))
if (RelationshipIsIncluded(r.InternalRelationshipName))
{
var navigationEntity = _jsonApiContext.ContextGraph
.GetRelationship(entity, r.InternalRelationshipName);

if(navigationEntity == null)
relationshipData.SingleData = null;
else if (navigationEntity is IEnumerable)
relationshipData.ManyData = _getRelationships((IEnumerable<object>)navigationEntity, r.InternalRelationshipName);
relationshipData.ManyData = GetRelationships((IEnumerable<object>)navigationEntity, r.InternalRelationshipName);
else
relationshipData.SingleData = _getRelationship(navigationEntity, r.InternalRelationshipName);
relationshipData.SingleData = GetRelationship(navigationEntity, r.InternalRelationshipName);
}

data.Relationships.Add(r.InternalRelationshipName.Dasherize(), relationshipData);
});
}

private List<DocumentData> _getIncludedEntities(ContextEntity contextEntity, IIdentifiable entity)
private List<DocumentData> GetIncludedEntities(ContextEntity contextEntity, IIdentifiable entity)
{
var included = new List<DocumentData>();

contextEntity.Relationships.ForEach(r =>
{
if (!_relationshipIsIncluded(r.InternalRelationshipName)) return;
if (!RelationshipIsIncluded(r.InternalRelationshipName)) return;

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

if (navigationEntity is IEnumerable)
foreach (var includedEntity in (IEnumerable)navigationEntity)
_addIncludedEntity(included, (IIdentifiable)includedEntity);
AddIncludedEntity(included, (IIdentifiable)includedEntity);
else
_addIncludedEntity(included, (IIdentifiable)navigationEntity);
AddIncludedEntity(included, (IIdentifiable)navigationEntity);
});

return included;
}

private void _addIncludedEntity(List<DocumentData> entities, IIdentifiable entity)
private void AddIncludedEntity(List<DocumentData> entities, IIdentifiable entity)
{
var includedEntity = _getIncludedEntity(entity);
var includedEntity = GetIncludedEntity(entity);
if(includedEntity != null)
entities.Add(includedEntity);
}

private DocumentData _getIncludedEntity(IIdentifiable entity)
private DocumentData GetIncludedEntity(IIdentifiable entity)
{
if(entity == null) return null;

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

var data = _getData(contextEntity, entity);
var data = GetData(contextEntity, entity);

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

Expand All @@ -202,13 +210,13 @@ private DocumentData _getIncludedEntity(IIdentifiable entity)
return data;
}

private bool _relationshipIsIncluded(string relationshipName)
private bool RelationshipIsIncluded(string relationshipName)
{
return _jsonApiContext.IncludedRelationships != null &&
_jsonApiContext.IncludedRelationships.Contains(relationshipName.ToProperCase());
}

private List<Dictionary<string, string>> _getRelationships(IEnumerable<object> entities, string relationshipName)
private List<Dictionary<string, string>> GetRelationships(IEnumerable<object> entities, string relationshipName)
{
var objType = entities.GetType().GenericTypeArguments[0];

Expand All @@ -224,7 +232,7 @@ private List<Dictionary<string, string>> _getRelationships(IEnumerable<object> e
}
return relationships;
}
private Dictionary<string, string> _getRelationship(object entity, string relationshipName)
private Dictionary<string, string> GetRelationship(object entity, string relationshipName)
{
var objType = entity.GetType();

Expand Down
6 changes: 3 additions & 3 deletions src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public DefaultEntityRepository(

public virtual IQueryable<TEntity> Get()
{
return _dbSet;
return _dbSet.Select(_jsonApiContext.QuerySet?.Fields);
}

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

public virtual async Task<TEntity> GetAsync(TId id)
{
return await _dbSet.SingleOrDefaultAsync(e => e.Id.Equals(id));
return await Get().SingleOrDefaultAsync(e => e.Id.Equals(id));
}

public virtual async Task<TEntity> GetAndIncludeAsync(TId id, string relationshipName)
{
return await _dbSet
return await Get()
.Include(relationshipName)
.SingleOrDefaultAsync(e => e.Id.Equals(id));
}
Expand Down
27 changes: 26 additions & 1 deletion src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
Expand Down Expand Up @@ -126,5 +126,30 @@ public static IQueryable<TSource> Filter<TSource>(this IQueryable<TSource> sourc
throw new JsonApiException("400", $"Could not cast {filterQuery.PropertyValue} to {property.PropertyType.Name}");
}
}
public static IQueryable<TSource> Select<TSource>(this IQueryable<TSource> source, IEnumerable<string> columns)
{
if(columns == null || columns.Count() == 0)
return source;

var sourceType = source.ElementType;

var resultType = typeof(TSource);

// {model}
var parameter = Expression.Parameter(sourceType, "model");

var bindings = columns.Select(column => Expression.Bind(
resultType.GetProperty(column), Expression.PropertyOrField(parameter, column)));

// { new Model () { Property = model.Property } }
var body = Expression.MemberInit(Expression.New(resultType), bindings);

// { model => new TodoItem() { Property = model.Property } }
var selector = Expression.Lambda(body, parameter);

return source.Provider.CreateQuery<TSource>(
Expression.Call(typeof(Queryable), "Select", new Type[] { sourceType, resultType },
source.Expression, Expression.Quote(selector)));
}
}
}
31 changes: 31 additions & 0 deletions src/JsonApiDotNetCore/Internal/Query/QuerySet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ public QuerySet(
_jsonApiContext = jsonApiContext;
PageQuery = new PageQuery();
Filters = new List<FilterQuery>();
Fields = new List<string>();
BuildQuerySet(query);
}

public List<FilterQuery> Filters { get; set; }
public PageQuery PageQuery { get; set; }
public List<SortQuery> SortParameters { get; set; }
public List<string> IncludedRelationships { get; set; }
public List<string> Fields { get; set; }

private void BuildQuerySet(IQueryCollection query)
{
Expand Down Expand Up @@ -55,6 +57,12 @@ private void BuildQuerySet(IQueryCollection query)
continue;
}

if (pair.Key.StartsWith("fields"))
{
Fields = ParseFieldsQuery(pair.Key, pair.Value);
continue;
}

throw new JsonApiException("400", $"{pair} is not a valid query.");
}
}
Expand Down Expand Up @@ -160,6 +168,29 @@ private List<string> ParseIncludedRelationships(string value)
.ToList();
}

private List<string> ParseFieldsQuery(string key, string value)
{
// expected: fields[TYPE]=prop1,prop2
var typeName = key.Split('[', ']')[1];

var includedFields = new List<string> { "Id" };

if(typeName != _jsonApiContext.RequestEntity.EntityName.Dasherize())
return includedFields;

var fields = value.Split(',');
foreach(var field in fields)
{
var internalAttrName = _jsonApiContext.RequestEntity
.Attributes
.SingleOrDefault(attr => attr.PublicAttributeName == field)
.InternalAttributeName;
includedFields.Add(internalAttrName);
}

return includedFields;
}

private AttrAttribute GetAttribute(string propertyName)
{
return _jsonApiContext.RequestEntity.Attributes
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
using DotNetCoreDocs;
using JsonApiDotNetCoreExample;
using DotNetCoreDocs.Writers;
using Newtonsoft.Json;
using JsonApiDotNetCore.Internal;
using JsonApiDotNetCore.Serialization;
using Xunit;
using System.Diagnostics;

namespace JsonApiDotNetCoreExampleTests.Acceptance.Extensibility
{
Expand All @@ -14,8 +10,6 @@ public class CustomErrorTests
[Fact]
public void Can_Return_Custom_Error_Types()
{
// while(!Debugger.IsAttached) { bool stop = false; }

// arrange
var error = new CustomError("507", "title", "detail", "custom");
var errorCollection = new ErrorCollection();
Expand Down
Loading