Skip to content

Nested sort #416

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 22 commits into from
Oct 18, 2018
Merged
Show file tree
Hide file tree
Changes from 21 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
3 changes: 3 additions & 0 deletions src/Examples/JsonApiDotNetCoreExample/Models/Person.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ public class Person : Identifiable, IHasMeta
[Attr("last-name")]
public string LastName { get; set; }

[Attr("age")]
public int Age { get; set; }

[HasMany("todo-items")]
public virtual List<TodoItem> TodoItems { get; set; }

Expand Down
26 changes: 13 additions & 13 deletions src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,10 @@ public virtual IQueryable<TEntity> Get()
/// <inheritdoc />
public virtual IQueryable<TEntity> Filter(IQueryable<TEntity> entities, FilterQuery filterQuery)
{
if(_resourceDefinition != null)
if (_resourceDefinition != null)
{
var defaultQueryFilters = _resourceDefinition.GetQueryFilters();
if(defaultQueryFilters != null && defaultQueryFilters.TryGetValue(filterQuery.Attribute, out var defaultQueryFilter) == true)
if (defaultQueryFilters != null && defaultQueryFilters.TryGetValue(filterQuery.Attribute, out var defaultQueryFilter) == true)
{
return defaultQueryFilter(entities, filterQuery.Value);
}
Expand All @@ -106,15 +106,15 @@ public virtual IQueryable<TEntity> Filter(IQueryable<TEntity> entities, FilterQu
public virtual IQueryable<TEntity> Sort(IQueryable<TEntity> entities, List<SortQuery> sortQueries)
{
if (sortQueries != null && sortQueries.Count > 0)
return entities.Sort(sortQueries);
if(_resourceDefinition != null)
return entities.Sort(_jsonApiContext, sortQueries);

if (_resourceDefinition != null)
{
var defaultSortOrder = _resourceDefinition.DefaultSort();
if(defaultSortOrder != null && defaultSortOrder.Count > 0)
if (defaultSortOrder != null && defaultSortOrder.Count > 0)
{
foreach(var sortProp in defaultSortOrder)
{
foreach (var sortProp in defaultSortOrder)
{
// this is dumb...add an overload, don't allocate for no reason
entities.Sort(new SortQuery(sortProp.Item2, sortProp.Item1));
}
Expand Down Expand Up @@ -189,10 +189,10 @@ private void AttachHasManyPointers(TEntity entity)
var relationships = _jsonApiContext.HasManyRelationshipPointers.Get();
foreach (var relationship in relationships)
{
if(relationship.Key is HasManyThroughAttribute hasManyThrough)
if (relationship.Key is HasManyThroughAttribute hasManyThrough)
AttachHasManyThrough(entity, hasManyThrough, relationship.Value);
else
AttachHasMany(relationship.Key as HasManyAttribute, relationship.Value);
AttachHasMany(relationship.Key as HasManyAttribute, relationship.Value);
}
}

Expand Down Expand Up @@ -289,15 +289,15 @@ public virtual async Task<bool> DeleteAsync(TId id)
/// <inheritdoc />
public virtual IQueryable<TEntity> Include(IQueryable<TEntity> entities, string relationshipName)
{
if(string.IsNullOrWhiteSpace(relationshipName)) throw new JsonApiException(400, "Include parameter must not be empty if provided");
if (string.IsNullOrWhiteSpace(relationshipName)) throw new JsonApiException(400, "Include parameter must not be empty if provided");

var relationshipChain = relationshipName.Split('.');

// variables mutated in recursive loop
// TODO: make recursive method
string internalRelationshipPath = null;
var entity = _jsonApiContext.RequestEntity;
for(var i = 0; i < relationshipChain.Length; i++)
for (var i = 0; i < relationshipChain.Length; i++)
{
var requestedRelationship = relationshipChain[i];
var relationship = entity.Relationships.FirstOrDefault(r => r.PublicRelationshipName == requestedRelationship);
Expand All @@ -315,7 +315,7 @@ public virtual IQueryable<TEntity> Include(IQueryable<TEntity> entities, string
internalRelationshipPath = (internalRelationshipPath == null)
? relationship.RelationshipPath
: $"{internalRelationshipPath}.{relationship.RelationshipPath}";

if(i < relationshipChain.Length)
entity = _jsonApiContext.ResourceGraph.GetContextEntity(relationship.Type);
}
Expand Down
329 changes: 187 additions & 142 deletions src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs

Large diffs are not rendered by default.

27 changes: 8 additions & 19 deletions src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs
Original file line number Diff line number Diff line change
@@ -1,37 +1,26 @@
using System.Linq;
using System;
using JsonApiDotNetCore.Models;
using JsonApiDotNetCore.Services;

namespace JsonApiDotNetCore.Internal.Query
{
public class AttrFilterQuery : BaseFilterQuery
{
private readonly IJsonApiContext _jsonApiContext;

public AttrFilterQuery(
IJsonApiContext jsonApiContext,
FilterQuery filterQuery)
: base(jsonApiContext, filterQuery)
{
_jsonApiContext = jsonApiContext;

var attribute = GetAttribute(filterQuery.Attribute);

if (attribute == null)
if (Attribute == null)
throw new JsonApiException(400, $"'{filterQuery.Attribute}' is not a valid attribute.");

if (attribute.IsFilterable == false)
throw new JsonApiException(400, $"Filter is not allowed for attribute '{attribute.PublicAttributeName}'.");
if (Attribute.IsFilterable == false)
throw new JsonApiException(400, $"Filter is not allowed for attribute '{Attribute.PublicAttributeName}'.");

FilteredAttribute = attribute;
PropertyValue = filterQuery.Value;
FilterOperation = GetFilterOperation(filterQuery.Operation);
FilteredAttribute = Attribute;
}

public AttrAttribute FilteredAttribute { get; }
public string PropertyValue { get; }
public FilterOperations FilterOperation { get; }

private AttrAttribute GetAttribute(string attribute) =>
_jsonApiContext.RequestEntity.Attributes.FirstOrDefault(attr => attr.Is(attribute));
[Obsolete("Use " + nameof(BaseAttrQuery.Attribute) + " insetad.")]
public AttrAttribute FilteredAttribute { get; set; }
}
}
23 changes: 23 additions & 0 deletions src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using JsonApiDotNetCore.Services;

namespace JsonApiDotNetCore.Internal.Query
{
public class AttrSortQuery : BaseAttrQuery
{
public AttrSortQuery(
IJsonApiContext jsonApiContext,
SortQuery sortQuery)
:base(jsonApiContext, sortQuery)
{
if (Attribute == null)
throw new JsonApiException(400, $"'{sortQuery.Attribute}' is not a valid attribute.");

if (Attribute.IsSortable == false)
throw new JsonApiException(400, $"Sort is not allowed for attribute '{Attribute.PublicAttributeName}'.");

Direction = sortQuery.Direction;
}

public SortDirection Direction { get; }
}
}
56 changes: 56 additions & 0 deletions src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using JsonApiDotNetCore.Models;
using JsonApiDotNetCore.Services;
using System.Linq;

namespace JsonApiDotNetCore.Internal.Query
{
/// <summary>
/// Abstract class to make available shared properties of all query implementations
/// It elimines boilerplate of providing specified type(AttrQuery or RelatedAttrQuery)
/// while filter and sort operations and eliminates plenty of methods to keep DRY principles
/// </summary>
public abstract class BaseAttrQuery
Copy link
Contributor Author

@milosloub milosloub Sep 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All shared properties (Attribute and Relationship) are located here. All implementations should use these default properties to keep DRY. Also this is the main class that can be cached for filter, sort and fields methods and make always one instance of Attribute and Relationship classes (in the future)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah I see, generally I only use abstract when I have abstract members and would use a protected constructor instead, but i think this is perfectly fine

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it depends on use case. I was inspired by EF approach and it's table inheritance - abstract base classes holds shared properties for all descendants and prevents creating new instances directly

https://www.learnentityframeworkcore.com/inheritance/table-per-hierarchy

{
private readonly IJsonApiContext _jsonApiContext;

public BaseAttrQuery(IJsonApiContext jsonApiContext, BaseQuery baseQuery)
{
_jsonApiContext = jsonApiContext;
Copy link
Contributor

@jaredcnance jaredcnance Oct 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's depend on the IContextGraph interface rather than IJsonApiContext..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I don't understand, but all the time AttrFilterQuery and RelatedAttrQuery depends on IJsonApiContext, so why IContextGraph?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, you're correct 😞 , the idea was to only depend on the interface that you actually need. #253 elaborates on this a bit more.

if (baseQuery.IsAttributeOfRelationship)
{
Relationship = GetRelationship(baseQuery.Relationship);
Attribute = GetAttribute(Relationship, baseQuery.Attribute);
}
else
{
Attribute = GetAttribute(baseQuery.Attribute);
}

}

public AttrAttribute Attribute { get; }
public RelationshipAttribute Relationship { get; }
public bool IsAttributeOfRelationship => Relationship != null;

public string GetPropertyPath()
{
if (IsAttributeOfRelationship)
return string.Format("{0}.{1}", Relationship.InternalRelationshipName, Attribute.InternalAttributeName);
else
return Attribute.InternalAttributeName;
}

private AttrAttribute GetAttribute(string attribute)
=> _jsonApiContext.RequestEntity.Attributes.FirstOrDefault(attr => attr.Is(attribute));

private RelationshipAttribute GetRelationship(string propertyName)
=> _jsonApiContext.RequestEntity.Relationships.FirstOrDefault(r => r.Is(propertyName));

private AttrAttribute GetAttribute(RelationshipAttribute relationship, string attribute)
{
var relatedContextExntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationship.Type);
return relatedContextExntity.Attributes
.FirstOrDefault(a => a.Is(attribute));
}
}
}
18 changes: 16 additions & 2 deletions src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
using JsonApiDotNetCore.Services;
using System;

namespace JsonApiDotNetCore.Internal.Query
{
public class BaseFilterQuery
public class BaseFilterQuery : BaseAttrQuery
{
protected FilterOperations GetFilterOperation(string prefix)
public BaseFilterQuery(
IJsonApiContext jsonApiContext,
FilterQuery filterQuery)
: base(jsonApiContext, filterQuery)
{
PropertyValue = filterQuery.Value;
FilterOperation = GetFilterOperation(filterQuery.Operation);
}

public string PropertyValue { get; }
public FilterOperations FilterOperation { get; }

private FilterOperations GetFilterOperation(string prefix)
{
if (prefix.Length == 0) return FilterOperations.eq;

Expand All @@ -13,5 +26,6 @@ protected FilterOperations GetFilterOperation(string prefix)

return opertion;
}

}
}
26 changes: 26 additions & 0 deletions src/JsonApiDotNetCore/Internal/Query/BaseQuery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using JsonApiDotNetCore.Models;
using JsonApiDotNetCore.Services;
using System;
using System.Linq;

namespace JsonApiDotNetCore.Internal.Query
{
public abstract class BaseQuery
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why abstract?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because concept is same as BaseAttrQuery, that only holds info about attributes, relationship etc. I don't know about use case, when client should create new instance of this BaseQuery, only descendant class (FilterQuery, SortQuery). Or do you have some idea where to use it without abstract?

{
public BaseQuery(string attribute)
{
var properties = attribute.Split(QueryConstants.DOT);
if(properties.Length > 1)
{
Relationship = properties[0];
Attribute = properties[1];
}
else
Attribute = properties[0];
}

public string Attribute { get; }
public string Relationship { get; }
public bool IsAttributeOfRelationship => Relationship != null;
}
}
10 changes: 5 additions & 5 deletions src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
using System;
using JsonApiDotNetCore.Extensions;
using JsonApiDotNetCore.Models;

namespace JsonApiDotNetCore.Internal.Query
{
public class FilterQuery
public class FilterQuery : BaseQuery
{
public FilterQuery(string attribute, string value, string operation)
: base(attribute)
{
Attribute = attribute;
Key = attribute.ToProperCase();
Value = value;
Operation = operation;
}

[Obsolete("Key has been replaced by '" + nameof(Attribute) + "'. Members should be located by their public name, not by coercing the provided value to the internal name.")]
public string Key { get; set; }
public string Attribute { get; }
public string Value { get; set; }
public string Operation { get; set; }
public bool IsAttributeOfRelationship => Attribute.Contains(".");

}
}
3 changes: 2 additions & 1 deletion src/JsonApiDotNetCore/Internal/Query/QueryConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public static class QueryConstants {
public const char COMMA = ',';
public const char COLON = ':';
public const string COLON_STR = ":";
public const char DOT = '.';

}
}
}
2 changes: 1 addition & 1 deletion src/JsonApiDotNetCore/Internal/Query/QuerySet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ public class QuerySet
public List<string> IncludedRelationships { get; set; } = new List<string>();
public List<string> Fields { get; set; } = new List<string>();
}
}
}
41 changes: 12 additions & 29 deletions src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs
Original file line number Diff line number Diff line change
@@ -1,50 +1,33 @@
using System.Linq;
using System;
using JsonApiDotNetCore.Models;
using JsonApiDotNetCore.Services;

namespace JsonApiDotNetCore.Internal.Query
{
public class RelatedAttrFilterQuery : BaseFilterQuery
{
private readonly IJsonApiContext _jsonApiContext;

public RelatedAttrFilterQuery(
IJsonApiContext jsonApiContext,
FilterQuery filterQuery)
:base(jsonApiContext, filterQuery)
{
_jsonApiContext = jsonApiContext;
if (Relationship == null)
throw new JsonApiException(400, $"{filterQuery.Relationship} is not a valid relationship on {jsonApiContext.RequestEntity.EntityName}.");

var relationshipArray = filterQuery.Attribute.Split('.');
var relationship = GetRelationship(relationshipArray[0]);
if (relationship == null)
throw new JsonApiException(400, $"{relationshipArray[1]} is not a valid relationship on {relationshipArray[0]}.");

var attribute = GetAttribute(relationship, relationshipArray[1]);
if (attribute == null)
if (Attribute == null)
throw new JsonApiException(400, $"'{filterQuery.Attribute}' is not a valid attribute.");

if (attribute.IsFilterable == false)
throw new JsonApiException(400, $"Filter is not allowed for attribute '{attribute.PublicAttributeName}'.");
if (Attribute.IsFilterable == false)
throw new JsonApiException(400, $"Filter is not allowed for attribute '{Attribute.PublicAttributeName}'.");

FilteredRelationship = relationship;
FilteredAttribute = attribute;
PropertyValue = filterQuery.Value;
FilterOperation = GetFilterOperation(filterQuery.Operation);
FilteredRelationship = Relationship;
FilteredAttribute = Attribute;
}

[Obsolete("Use " + nameof(Attribute) + " instead.")]
public AttrAttribute FilteredAttribute { get; set; }
public string PropertyValue { get; set; }
public FilterOperations FilterOperation { get; set; }
public RelationshipAttribute FilteredRelationship { get; }

private RelationshipAttribute GetRelationship(string propertyName)
=> _jsonApiContext.RequestEntity.Relationships.FirstOrDefault(r => r.Is(propertyName));

private AttrAttribute GetAttribute(RelationshipAttribute relationship, string attribute)
{
var relatedContextExntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationship.Type);
return relatedContextExntity.Attributes
.FirstOrDefault(a => a.Is(attribute));
}
[Obsolete("Use " + nameof(Relationship) + " instead.")]
public RelationshipAttribute FilteredRelationship { get; set; }
}
}
Loading