From 94fdff24d6ac745597745ab173d332a7046001b5 Mon Sep 17 00:00:00 2001 From: Milos Date: Mon, 24 Sep 2018 20:34:06 +0200 Subject: [PATCH 01/18] Nested sort - Init --- .../JsonApiDotNetCoreExample/Models/Person.cs | 3 + .../Builders/DocumentBuilder.cs | 2 +- .../Data/DefaultEntityRepository.cs | 4 +- .../Extensions/IQueryableExtensions.cs | 179 +++++++++++++----- .../Internal/Query/AttrFilterQuery.cs | 24 +-- .../Internal/Query/AttrQuery.cs | 36 ++++ .../Internal/Query/AttrSortQuery.cs | 23 +++ .../Internal/Query/BaseFilterQuery.cs | 17 -- .../Internal/Query/FilterOperations.cs | 33 +++- .../Internal/Query/FilterQuery.cs | 7 +- .../Internal/Query/QueryAttribute.cs | 26 +++ .../Internal/Query/QuerySet.cs | 4 +- .../Internal/Query/RelatedAttrFilterQuery.cs | 37 +--- .../Internal/Query/RelatedAttrQuery.cs | 51 +++++ .../Internal/Query/RelatedAttrSortQuery.cs | 24 +++ .../Internal/Query/SortQuery.cs | 9 +- src/JsonApiDotNetCore/Services/QueryParser.cs | 90 +++++---- .../Acceptance/Spec/SparseFieldSetTests.cs | 40 +++- .../Acceptance/TodoItemsControllerTests.cs | 82 ++++++++ test/UnitTests/Services/QueryParser_Tests.cs | 6 +- 20 files changed, 517 insertions(+), 180 deletions(-) create mode 100644 src/JsonApiDotNetCore/Internal/Query/AttrQuery.cs create mode 100644 src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs delete mode 100644 src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs create mode 100644 src/JsonApiDotNetCore/Internal/Query/QueryAttribute.cs create mode 100644 src/JsonApiDotNetCore/Internal/Query/RelatedAttrQuery.cs create mode 100644 src/JsonApiDotNetCore/Internal/Query/RelatedAttrSortQuery.cs diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs index fdb2f1f174..eb032cc5fd 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs @@ -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 TodoItems { get; set; } diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index 6afa13e029..cfbd6c21e9 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -142,7 +142,7 @@ private bool ShouldIncludeAttribute(AttrAttribute attr, object attributeValue) return OmitNullValuedAttribute(attr, attributeValue) == false && ((_jsonApiContext.QuerySet == null || _jsonApiContext.QuerySet.Fields.Count == 0) - || _jsonApiContext.QuerySet.Fields.Contains(attr.InternalAttributeName)); + || _jsonApiContext.QuerySet.Fields.Any(i => i.Attribute == attr.InternalAttributeName)); } private bool OmitNullValuedAttribute(AttrAttribute attr, object attributeValue) diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 512f45fe3c..fde424fa53 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -58,7 +58,7 @@ public DefaultEntityRepository( public virtual IQueryable Get() { if (_jsonApiContext.QuerySet?.Fields != null && _jsonApiContext.QuerySet.Fields.Count > 0) - return _dbSet.Select(_jsonApiContext.QuerySet?.Fields); + return _dbSet.Select(_jsonApiContext.QuerySet.Fields); return _dbSet; } @@ -72,7 +72,7 @@ public virtual IQueryable Filter(IQueryable entities, FilterQu /// public virtual IQueryable Sort(IQueryable entities, List sortQueries) { - return entities.Sort(sortQueries); + return entities.Sort(_jsonApiContext, sortQueries); } /// diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index 8afab4d45a..11e206e1e3 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -5,6 +5,7 @@ using System.Reflection; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; namespace JsonApiDotNetCore.Extensions @@ -28,68 +29,118 @@ private static MethodInfo ContainsMethod } } - - public static IQueryable Sort(this IQueryable source, List sortQueries) + public static IQueryable Sort(this IQueryable source, IJsonApiContext jsonApiContext, List sortQueries) { if (sortQueries == null || sortQueries.Count == 0) return source; - var orderedEntities = source.Sort(sortQueries[0]); + var orderedEntities = source.Sort(jsonApiContext, sortQueries[0]); - if (sortQueries.Count <= 1) return orderedEntities; + if (sortQueries.Count <= 1) + return orderedEntities; for (var i = 1; i < sortQueries.Count; i++) - orderedEntities = orderedEntities.Sort(sortQueries[i]); + orderedEntities = orderedEntities.Sort(jsonApiContext, sortQueries[i]); return orderedEntities; } - public static IOrderedQueryable Sort(this IQueryable source, SortQuery sortQuery) + public static IOrderedQueryable Sort(this IQueryable source, IJsonApiContext jsonApiContext, SortQuery sortQuery) { - return sortQuery.Direction == SortDirection.Descending - ? source.OrderByDescending(sortQuery.SortedAttribute.InternalAttributeName) - : source.OrderBy(sortQuery.SortedAttribute.InternalAttributeName); + if (sortQuery.IsAttributeOfRelationship) + { + var relatedAttrQuery = new RelatedAttrSortQuery(jsonApiContext, sortQuery); + return sortQuery.Direction == SortDirection.Descending + ? source.OrderByDescending(relatedAttrQuery) + : source.OrderBy(relatedAttrQuery); + } + else + { + var attrQuery = new AttrSortQuery(jsonApiContext, sortQuery); + return sortQuery.Direction == SortDirection.Descending + ? source.OrderByDescending(attrQuery) + : source.OrderBy(attrQuery); + } } - public static IOrderedQueryable Sort(this IOrderedQueryable source, SortQuery sortQuery) + public static IOrderedQueryable Sort(this IOrderedQueryable source, IJsonApiContext jsonApiContext, SortQuery sortQuery) { - return sortQuery.Direction == SortDirection.Descending - ? source.ThenByDescending(sortQuery.SortedAttribute.InternalAttributeName) - : source.ThenBy(sortQuery.SortedAttribute.InternalAttributeName); + if (sortQuery.IsAttributeOfRelationship) + { + var relatedAttrQuery = new RelatedAttrSortQuery(jsonApiContext, sortQuery); + return sortQuery.Direction == SortDirection.Descending + ? source.OrderByDescending(relatedAttrQuery) + : source.OrderBy(relatedAttrQuery); + } + else + { + var attrQuery = new AttrSortQuery(jsonApiContext, sortQuery); + return sortQuery.Direction == SortDirection.Descending + ? source.OrderByDescending(attrQuery) + : source.OrderBy(attrQuery); + } } - public static IOrderedQueryable OrderBy(this IQueryable source, string propertyName) + public static IOrderedQueryable OrderBy(this IQueryable source, AttrQuery attrQuery) + { + return CallGenericOrderMethod(source, attrQuery.Attribute, null, "OrderBy"); + } + public static IOrderedQueryable OrderBy(this IQueryable source, RelatedAttrQuery relatedAttrQuery) { - return CallGenericOrderMethod(source, propertyName, "OrderBy"); + return CallGenericOrderMethod(source, relatedAttrQuery.Attribute, relatedAttrQuery.RelationshipAttribute, "OrderBy"); } - public static IOrderedQueryable OrderByDescending(this IQueryable source, string propertyName) + public static IOrderedQueryable OrderByDescending(this IQueryable source, AttrQuery attrQuery) { - return CallGenericOrderMethod(source, propertyName, "OrderByDescending"); + return CallGenericOrderMethod(source, attrQuery.Attribute, null, "OrderByDescending"); + } + public static IOrderedQueryable OrderByDescending(this IQueryable source, RelatedAttrQuery relatedAttrQuery) + { + return CallGenericOrderMethod(source, relatedAttrQuery.Attribute, relatedAttrQuery.RelationshipAttribute, "OrderByDescending"); } - public static IOrderedQueryable ThenBy(this IOrderedQueryable source, string propertyName) + public static IOrderedQueryable ThenBy(this IOrderedQueryable source, AttrQuery attrQuery) + { + return CallGenericOrderMethod(source, attrQuery.Attribute, null, "ThenBy"); + } + public static IOrderedQueryable ThenBy(this IOrderedQueryable source, RelatedAttrQuery relatedAttrQuery) { - return CallGenericOrderMethod(source, propertyName, "ThenBy"); + return CallGenericOrderMethod(source, relatedAttrQuery.Attribute, relatedAttrQuery.RelationshipAttribute, "ThenBy"); } - public static IOrderedQueryable ThenByDescending(this IOrderedQueryable source, string propertyName) + public static IOrderedQueryable ThenByDescending(this IOrderedQueryable source, AttrQuery attrQuery) { - return CallGenericOrderMethod(source, propertyName, "ThenByDescending"); + return CallGenericOrderMethod(source, attrQuery.Attribute, null, "ThenByDescending"); + } + public static IOrderedQueryable ThenByDescending(this IOrderedQueryable source, RelatedAttrQuery relatedAttrQuery) + { + return CallGenericOrderMethod(source, relatedAttrQuery.Attribute, relatedAttrQuery.RelationshipAttribute, "ThenByDescending"); } - private static IOrderedQueryable CallGenericOrderMethod(IQueryable source, string propertyName, string method) + private static IOrderedQueryable CallGenericOrderMethod(IQueryable source, AttrAttribute attr, RelationshipAttribute relationAttr, string method) { // {x} var parameter = Expression.Parameter(typeof(TSource), "x"); + + //var property = Expression.Property(parameter, attr.InternalAttributeName); + + MemberExpression member; + // {x.relationship.propertyName} + if (relationAttr != null) + { + var relation = Expression.PropertyOrField(parameter, relationAttr.InternalRelationshipName); + member = Expression.Property(relation, attr.InternalAttributeName); + } // {x.propertyName} - var property = Expression.Property(parameter, propertyName); - // {x=>x.propertyName} - var lambda = Expression.Lambda(property, parameter); + else + member = Expression.Property(parameter, attr.InternalAttributeName); + + // {x=>x.propertyName} or {x=>x.relationship.propertyName} + var lambda = Expression.Lambda(member, parameter); // REFLECTION: source.OrderBy(x => x.Property) var orderByMethod = typeof(Queryable).GetMethods().First(x => x.Name == method && x.GetParameters().Length == 2); - var orderByGeneric = orderByMethod.MakeGenericMethod(typeof(TSource), property.Type); + var orderByGeneric = orderByMethod.MakeGenericMethod(typeof(TSource), member.Type); var result = orderByGeneric.Invoke(null, new object[] { source, lambda }); return (IOrderedQueryable)result; @@ -112,22 +163,22 @@ public static IQueryable Filter(this IQueryable sourc return source; var concreteType = typeof(TSource); - var property = concreteType.GetProperty(filterQuery.FilteredAttribute.InternalAttributeName); + var property = concreteType.GetProperty(filterQuery.Attribute.InternalAttributeName); var op = filterQuery.FilterOperation; if (property == null) - throw new ArgumentException($"'{filterQuery.FilteredAttribute.InternalAttributeName}' is not a valid property of '{concreteType}'"); + throw new ArgumentException($"'{filterQuery.Attribute.InternalAttributeName}' is not a valid property of '{concreteType}'"); try { - if (op == FilterOperations.@in || op == FilterOperations.nin) + if (op == FilterOperationsEnum.@in || op == FilterOperationsEnum.nin) { string[] propertyValues = filterQuery.PropertyValue.Split(','); var lambdaIn = ArrayContainsPredicate(propertyValues, property.Name, op); return source.Where(lambdaIn); } - else if (op == FilterOperations.isnotnull || op == FilterOperations.isnull) { + else if (op == FilterOperationsEnum.isnotnull || op == FilterOperationsEnum.isnull) { // {model} var parameter = Expression.Parameter(concreteType, "model"); // {model.Id} @@ -169,18 +220,18 @@ public static IQueryable Filter(this IQueryable sourc return source; var concreteType = typeof(TSource); - var relation = concreteType.GetProperty(filterQuery.FilteredRelationship.InternalRelationshipName); + var relation = concreteType.GetProperty(filterQuery.RelationshipAttribute.InternalRelationshipName); if (relation == null) - throw new ArgumentException($"'{filterQuery.FilteredRelationship.InternalRelationshipName}' is not a valid relationship of '{concreteType}'"); + throw new ArgumentException($"'{filterQuery.RelationshipAttribute.InternalRelationshipName}' is not a valid relationship of '{concreteType}'"); - var relatedType = filterQuery.FilteredRelationship.Type; - var relatedAttr = relatedType.GetProperty(filterQuery.FilteredAttribute.InternalAttributeName); + var relatedType = filterQuery.RelationshipAttribute.Type; + var relatedAttr = relatedType.GetProperty(filterQuery.Attribute.InternalAttributeName); if (relatedAttr == null) - throw new ArgumentException($"'{filterQuery.FilteredAttribute.InternalAttributeName}' is not a valid attribute of '{filterQuery.FilteredRelationship.InternalRelationshipName}'"); + throw new ArgumentException($"'{filterQuery.Attribute.InternalAttributeName}' is not a valid attribute of '{filterQuery.RelationshipAttribute.InternalRelationshipName}'"); try { - if (filterQuery.FilterOperation == FilterOperations.@in || filterQuery.FilterOperation == FilterOperations.nin) + if (filterQuery.FilterOperation == FilterOperationsEnum.@in || filterQuery.FilterOperation == FilterOperationsEnum.nin) { string[] propertyValues = filterQuery.PropertyValue.Split(','); var lambdaIn = ArrayContainsPredicate(propertyValues, relatedAttr.Name, filterQuery.FilterOperation, relation.Name); @@ -220,43 +271,43 @@ public static IQueryable Filter(this IQueryable sourc private static bool IsNullable(Type type) => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); - private static Expression GetFilterExpressionLambda(Expression left, Expression right, FilterOperations operation) + private static Expression GetFilterExpressionLambda(Expression left, Expression right, FilterOperationsEnum operation) { Expression body; switch (operation) { - case FilterOperations.eq: + case FilterOperationsEnum.eq: // {model.Id == 1} body = Expression.Equal(left, right); break; - case FilterOperations.lt: + case FilterOperationsEnum.lt: // {model.Id < 1} body = Expression.LessThan(left, right); break; - case FilterOperations.gt: + case FilterOperationsEnum.gt: // {model.Id > 1} body = Expression.GreaterThan(left, right); break; - case FilterOperations.le: + case FilterOperationsEnum.le: // {model.Id <= 1} body = Expression.LessThanOrEqual(left, right); break; - case FilterOperations.ge: + case FilterOperationsEnum.ge: // {model.Id >= 1} body = Expression.GreaterThanOrEqual(left, right); break; - case FilterOperations.like: + case FilterOperationsEnum.like: body = Expression.Call(left, "Contains", null, right); break; // {model.Id != 1} - case FilterOperations.ne: + case FilterOperationsEnum.ne: body = Expression.NotEqual(left, right); break; - case FilterOperations.isnotnull: + case FilterOperationsEnum.isnotnull: // {model.Id != null} body = Expression.NotEqual(left, right); break; - case FilterOperations.isnull: + case FilterOperationsEnum.isnull: // {model.Id == null} body = Expression.Equal(left, right); break; @@ -267,7 +318,7 @@ private static Expression GetFilterExpressionLambda(Expression left, Expression return body; } - private static Expression> ArrayContainsPredicate(string[] propertyValues, string fieldname, FilterOperations op, string relationName = null) + private static Expression> ArrayContainsPredicate(string[] propertyValues, string fieldname, FilterOperationsEnum op, string relationName = null) { ParameterExpression entity = Expression.Parameter(typeof(TSource), "entity"); MemberExpression member; @@ -282,7 +333,7 @@ private static Expression> ArrayContainsPredicate(s var method = ContainsMethod.MakeGenericMethod(member.Type); var obj = TypeHelper.ConvertListType(propertyValues, member.Type); - if (op == FilterOperations.@in) + if (op == FilterOperationsEnum.@in) { // Where(i => arr.Contains(i.column)) var contains = Expression.Call(method, new Expression[] { Expression.Constant(obj), member }); @@ -296,20 +347,44 @@ private static Expression> ArrayContainsPredicate(s } } - public static IQueryable Select(this IQueryable source, List columns) + public static IQueryable Select(this IQueryable source, List 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 attrs = new List(); + // Key = Relationship, Value = Attribute + var relationAttrs = new Dictionary(); + foreach(var item in columns) + { + if (item.IsAttributeOfRelationship) + relationAttrs.Add(item.RelationshipAttribute, item.Attribute) ; + else + attrs.Add(item.Attribute); + } + + var bindings = new List(); + bindings.AddRange(attrs.Select(column => Expression.Bind(resultType.GetProperty(column), Expression.PropertyOrField(parameter, column)))); + + + //foreach (var relationAttr in relationAttrs) + //{ + // var relation = Expression.PropertyOrField(parameter, relationAttr.Key); + // var member = Expression.Property(relation, relationAttr.Value); + // var relationType = member.Type; + + // var relationshipBindings = new List(); + // relationshipBindings.AddRange(attrs.Select(column => Expression.Bind(resultType.GetProperty(column), Expression.PropertyOrField(parameter, column)))); - var bindings = columns.Select(column => Expression.Bind( - resultType.GetProperty(column), Expression.PropertyOrField(parameter, column))); + // var body = Expression.MemberInit(Expression.New(relation.Type), bindings); + // var ah = Expression.Bind(relation.Member, member.Expression); + // bindings.Add(ah); + //} // { new Model () { Property = model.Property } } var body = Expression.MemberInit(Expression.New(resultType), bindings); diff --git a/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs index a914fea3a7..d1d7d1d32d 100644 --- a/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs @@ -4,34 +4,22 @@ namespace JsonApiDotNetCore.Internal.Query { - public class AttrFilterQuery : BaseFilterQuery + public class AttrFilterQuery : AttrQuery { - private readonly IJsonApiContext _jsonApiContext; - public AttrFilterQuery( IJsonApiContext jsonApiContext, FilterQuery filterQuery) + :base(jsonApiContext, filterQuery) { - _jsonApiContext = jsonApiContext; - - var attribute = GetAttribute(filterQuery.Attribute); - - 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); + FilterOperation = FilterOperations.GetFilterOperation(filterQuery.Operation); } - public AttrAttribute FilteredAttribute { get; } public string PropertyValue { get; } - public FilterOperations FilterOperation { get; } + public FilterOperationsEnum FilterOperation { get; } - private AttrAttribute GetAttribute(string attribute) => - _jsonApiContext.RequestEntity.Attributes.FirstOrDefault(attr => attr.Is(attribute)); } } diff --git a/src/JsonApiDotNetCore/Internal/Query/AttrQuery.cs b/src/JsonApiDotNetCore/Internal/Query/AttrQuery.cs new file mode 100644 index 0000000000..8ebde76db8 --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Query/AttrQuery.cs @@ -0,0 +1,36 @@ +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; +using System; +using System.Linq; + +namespace JsonApiDotNetCore.Internal.Query +{ + public class AttrQuery + { + private readonly IJsonApiContext _jsonApiContext; + + public AttrQuery(IJsonApiContext jsonApiContext, QueryAttribute query) + { + _jsonApiContext = jsonApiContext; + Attribute = GetAttribute(query.Attribute); + } + + public AttrAttribute Attribute { get; } + + private AttrAttribute GetAttribute(string attribute) + { + try + { + return _jsonApiContext + .RequestEntity + .Attributes + .Single(attr => attr.Is(attribute)); + } + catch (InvalidOperationException e) + { + throw new JsonApiException(400, $"Attribute '{attribute}' does not exist on resource '{_jsonApiContext.RequestEntity.EntityName}'", e); + } + } + + } +} diff --git a/src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs b/src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs new file mode 100644 index 0000000000..19f76a95e9 --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs @@ -0,0 +1,23 @@ +using System.Linq; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; + +namespace JsonApiDotNetCore.Internal.Query +{ + public class AttrSortQuery : AttrQuery + { + public AttrSortQuery( + IJsonApiContext jsonApiContext, + SortQuery sortQuery) + :base(jsonApiContext, sortQuery) + { + if (Attribute.IsSortable == false) + throw new JsonApiException(400, $"Sort is not allowed for attribute '{Attribute.PublicAttributeName}'."); + + Direction = sortQuery.Direction; + } + + public SortDirection Direction { get; set; } + + } +} diff --git a/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs deleted file mode 100644 index 1c43d84254..0000000000 --- a/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace JsonApiDotNetCore.Internal.Query -{ - public class BaseFilterQuery - { - protected FilterOperations GetFilterOperation(string prefix) - { - if (prefix.Length == 0) return FilterOperations.eq; - - if (Enum.TryParse(prefix, out FilterOperations opertion) == false) - throw new JsonApiException(400, $"Invalid filter prefix '{prefix}'"); - - return opertion; - } - } -} diff --git a/src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs b/src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs index 60ae0af012..b4171284d3 100644 --- a/src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs +++ b/src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs @@ -1,7 +1,9 @@ // ReSharper disable InconsistentNaming +using System; + namespace JsonApiDotNetCore.Internal.Query { - public enum FilterOperations + public enum FilterOperationsEnum { eq = 0, lt = 1, @@ -15,4 +17,33 @@ public enum FilterOperations isnull = 9, isnotnull = 10 } + + public class FilterOperations + { + public static FilterOperationsEnum GetFilterOperation(string prefix) + { + if (prefix.Length == 0) return FilterOperationsEnum.eq; + + if (Enum.TryParse(prefix, out FilterOperationsEnum opertion) == false) + throw new JsonApiException(400, $"Invalid filter prefix '{prefix}'"); + + return opertion; + } + + public static string GetFilterOperationFromQuery(string query) + { + var values = query.Split(QueryConstants.COLON); + + if (values.Length == 1) + return string.Empty; + + var operation = values[0]; + // remove prefix from value + if (Enum.TryParse(operation, out FilterOperationsEnum op) == false) + return string.Empty; + + return operation; + } + + } } diff --git a/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs index dd72e827ff..852af1e55e 100644 --- a/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs @@ -3,11 +3,11 @@ namespace JsonApiDotNetCore.Internal.Query { - public class FilterQuery + public class FilterQuery: QueryAttribute { public FilterQuery(string attribute, string value, string operation) + :base(attribute) { - Attribute = attribute; Key = attribute.ToProperCase(); Value = value; Operation = operation; @@ -15,9 +15,8 @@ public FilterQuery(string attribute, string value, string 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("."); + } } diff --git a/src/JsonApiDotNetCore/Internal/Query/QueryAttribute.cs b/src/JsonApiDotNetCore/Internal/Query/QueryAttribute.cs new file mode 100644 index 0000000000..3b6cc0a099 --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Query/QueryAttribute.cs @@ -0,0 +1,26 @@ +namespace JsonApiDotNetCore.Internal.Query +{ + public class QueryAttribute + { + public QueryAttribute(string attribute) + { + var attributes = attribute.Split('.'); + if (attributes.Length > 1) + { + RelationshipAttribute = attributes[0]; + Attribute = attributes[1]; + IsAttributeOfRelationship = true; + } + else + { + Attribute = attribute; + IsAttributeOfRelationship = false; + } + + } + + public string Attribute { get; } + public string RelationshipAttribute { get; } + public bool IsAttributeOfRelationship { get; } + } +} diff --git a/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs b/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs index 88aac1e67b..de89c0cdf6 100644 --- a/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs +++ b/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs @@ -8,6 +8,6 @@ public class QuerySet public PageQuery PageQuery { get; set; } = new PageQuery(); public List SortParameters { get; set; } = new List(); public List IncludedRelationships { get; set; } = new List(); - public List Fields { get; set; } = new List(); + public List Fields { get; set; } = new List(); } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs index 9b117c0913..67ce619a9b 100644 --- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs @@ -4,47 +4,20 @@ namespace JsonApiDotNetCore.Internal.Query { - public class RelatedAttrFilterQuery : BaseFilterQuery + public class RelatedAttrFilterQuery : RelatedAttrQuery { - private readonly IJsonApiContext _jsonApiContext; - + public RelatedAttrFilterQuery( IJsonApiContext jsonApiContext, FilterQuery filterQuery) + :base(jsonApiContext, filterQuery) { - _jsonApiContext = jsonApiContext; - - 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) - 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}'."); - - FilteredRelationship = relationship; - FilteredAttribute = attribute; PropertyValue = filterQuery.Value; - FilterOperation = GetFilterOperation(filterQuery.Operation); + FilterOperation = FilterOperations.GetFilterOperation(filterQuery.Operation); } - public AttrAttribute FilteredAttribute { get; set; } public string PropertyValue { get; set; } - public FilterOperations FilterOperation { get; set; } - public RelationshipAttribute FilteredRelationship { get; } + public FilterOperationsEnum FilterOperation { get; set; } - private RelationshipAttribute GetRelationship(string propertyName) - => _jsonApiContext.RequestEntity.Relationships.FirstOrDefault(r => r.Is(propertyName)); - - private AttrAttribute GetAttribute(RelationshipAttribute relationship, string attribute) - { - var relatedContextExntity = _jsonApiContext.ContextGraph.GetContextEntity(relationship.Type); - return relatedContextExntity.Attributes - .FirstOrDefault(a => a.Is(attribute)); - } } } diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrQuery.cs new file mode 100644 index 0000000000..dda19ff5ce --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrQuery.cs @@ -0,0 +1,51 @@ +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; +using System; +using System.Linq; + +namespace JsonApiDotNetCore.Internal.Query +{ + public class RelatedAttrQuery + { + private readonly IJsonApiContext _jsonApiContext; + + public RelatedAttrQuery(IJsonApiContext jsonApiContext, QueryAttribute query) + { + _jsonApiContext = jsonApiContext; + + RelationshipAttribute = GetRelationshipAttribute(query.RelationshipAttribute); + Attribute = GetAttribute(RelationshipAttribute, query.Attribute); + } + + public AttrAttribute Attribute { get; } + public RelationshipAttribute RelationshipAttribute { get; } + + private RelationshipAttribute GetRelationshipAttribute(string relationship) + { + try + { + return _jsonApiContext + .RequestEntity + .Relationships + .Single(attr => attr.Is(relationship)); + } + catch (InvalidOperationException e) + { + throw new JsonApiException(400, $"Relationship '{relationship}' does not exist on resource '{_jsonApiContext.RequestEntity.EntityName}'", e); + } + } + + private AttrAttribute GetAttribute(RelationshipAttribute relationship, string attribute) + { + var relatedContextExntity = _jsonApiContext.ContextGraph.GetContextEntity(relationship.Type); + try + { + return relatedContextExntity.Attributes.Single(attr => attr.Is(attribute)); + } + catch (InvalidOperationException e) + { + throw new JsonApiException(400, $"Attribute '{attribute}' does not exist on resource '{relatedContextExntity.EntityName}'", e); + } + } + } +} diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrSortQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrSortQuery.cs new file mode 100644 index 0000000000..58422acb99 --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrSortQuery.cs @@ -0,0 +1,24 @@ +using System.Linq; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; + +namespace JsonApiDotNetCore.Internal.Query +{ + public class RelatedAttrSortQuery : RelatedAttrQuery + { + + public RelatedAttrSortQuery( + IJsonApiContext jsonApiContext, + SortQuery sortQuery) + :base(jsonApiContext, sortQuery) + { + if (Attribute.IsSortable == false) + throw new JsonApiException(400, $"Sort is not allowed for attribute '{Attribute.PublicAttributeName}' on relationship '{RelationshipAttribute.PublicRelationshipName}'."); + + Direction = sortQuery.Direction; + } + + public SortDirection Direction { get; set; } + + } +} diff --git a/src/JsonApiDotNetCore/Internal/Query/SortQuery.cs b/src/JsonApiDotNetCore/Internal/Query/SortQuery.cs index 7ef6682cc6..4bacd60431 100644 --- a/src/JsonApiDotNetCore/Internal/Query/SortQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/SortQuery.cs @@ -2,14 +2,13 @@ namespace JsonApiDotNetCore.Internal.Query { - public class SortQuery + public class SortQuery: QueryAttribute { - public SortQuery(SortDirection direction, AttrAttribute sortedAttribute) + public SortQuery(SortDirection direction, string sortedAttribute) + :base(sortedAttribute) { Direction = direction; - SortedAttribute = sortedAttribute; } public SortDirection Direction { get; set; } - public AttrAttribute SortedAttribute { get; set; } } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Services/QueryParser.cs b/src/JsonApiDotNetCore/Services/QueryParser.cs index b42d616a0c..93e2d60d07 100644 --- a/src/JsonApiDotNetCore/Services/QueryParser.cs +++ b/src/JsonApiDotNetCore/Services/QueryParser.cs @@ -85,9 +85,9 @@ protected virtual List ParseFilterQuery(string key, string value) var propertyName = key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1]; // InArray case - string op = GetFilterOperation(value); - if (string.Equals(op, FilterOperations.@in.ToString(), StringComparison.OrdinalIgnoreCase) - || string.Equals(op, FilterOperations.nin.ToString(), StringComparison.OrdinalIgnoreCase)) + string op = FilterOperations.GetFilterOperationFromQuery(value); + if (string.Equals(op, FilterOperationsEnum.@in.ToString(), StringComparison.OrdinalIgnoreCase) + || string.Equals(op, FilterOperationsEnum.nin.ToString(), StringComparison.OrdinalIgnoreCase)) { (var operation, var filterValue) = ParseFilterOperation(value); queries.Add(new FilterQuery(propertyName, filterValue, op)); @@ -110,7 +110,7 @@ protected virtual (string operation, string value) ParseFilterOperation(string v if (value.Length < 3) return (string.Empty, value); - var operation = GetFilterOperation(value); + var operation = FilterOperations.GetFilterOperationFromQuery(value); var values = value.Split(QueryConstants.COLON); if (string.IsNullOrEmpty(operation)) @@ -166,12 +166,7 @@ protected virtual List ParseSortParameters(string value) propertyName = propertyName.Substring(1); } - var attribute = GetAttribute(propertyName); - - if (attribute.IsSortable == false) - throw new JsonApiException(400, $"Sort is not allowed for attribute '{attribute.PublicAttributeName}'."); - - sortParameters.Add(new SortQuery(direction, attribute)); + sortParameters.Add(new SortQuery(direction, propertyName)); }; return sortParameters; @@ -184,13 +179,11 @@ protected virtual List ParseIncludedRelationships(string value) .ToList(); } - protected virtual List ParseFieldsQuery(string key, string value) + protected virtual List ParseFieldsQuery(string key, string value) { // expected: fields[TYPE]=prop1,prop2 var typeName = key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1]; - - const string ID = "Id"; - var includedFields = new List { ID }; + var includedFields = new List { new QueryAttribute(nameof(Identifiable.Id)) }; // this will not support nested inclusions, it requires that the typeName is the current request type if (string.Equals(typeName, _controllerContext.RequestEntity.EntityName, StringComparison.OrdinalIgnoreCase) == false) @@ -199,53 +192,68 @@ protected virtual List ParseFieldsQuery(string key, string value) var fields = value.Split(QueryConstants.COMMA); foreach (var field in fields) { - var attr = _controllerContext.RequestEntity - .Attributes - .SingleOrDefault(a => a.Is(field)); + string queryField = null; + if(field.Contains('.')) + { + var properties = field.Split('.'); + var relationship = GetRelationshipAttribute(properties[0]); - if (attr == null) throw new JsonApiException(400, $"'{_controllerContext.RequestEntity.EntityName}' does not contain '{field}'."); + // Temp method - no chance to get attribute of relationship on Controller level + queryField = relationship.InternalRelationshipName + "." + UppercaseFirst(properties[1]); + } + else + { + var attr = GetAttribute(field); + queryField = attr.InternalAttributeName; + } - var internalAttrName = attr.InternalAttributeName; - includedFields.Add(internalAttrName); + // Store Internal attributes names + includedFields.Add(new QueryAttribute(queryField)); } return includedFields; } + + private string UppercaseFirst(string s) + { + // Check for empty string. + if (string.IsNullOrEmpty(s)) + { + return string.Empty; + } + // Return char and concat substring. + return char.ToUpper(s[0]) + s.Substring(1); + } - protected virtual AttrAttribute GetAttribute(string propertyName) + protected virtual RelationshipAttribute GetRelationshipAttribute(string relationship) { try { return _controllerContext .RequestEntity - .Attributes - .Single(attr => attr.Is(propertyName)); + .Relationships + .Single(attr => attr.Is(relationship)); } catch (InvalidOperationException e) { - throw new JsonApiException(400, $"Attribute '{propertyName}' does not exist on resource '{_controllerContext.RequestEntity.EntityName}'", e); + throw new JsonApiException(400, $"Relationship '{relationship}' does not exist on resource '{_controllerContext.RequestEntity.EntityName}'", e); } } - private string GetFilterOperation(string value) + protected virtual AttrAttribute GetAttribute(string attribute) { - var values = value.Split(QueryConstants.COLON); - - if (values.Length == 1) - return string.Empty; - - var operation = values[0]; - // remove prefix from value - if (Enum.TryParse(operation, out FilterOperations op) == false) - return string.Empty; - - return operation; + try + { + return _controllerContext + .RequestEntity + .Attributes + .Single(attr => attr.Is(attribute)); + } + catch (InvalidOperationException e) + { + throw new JsonApiException(400, $"Attribute '{attribute}' does not exist on resource '{_controllerContext.RequestEntity.EntityName}'", e); + } } - private FilterQuery BuildFilterQuery(ReadOnlySpan query, string propertyName) - { - var (operation, filterValue) = ParseFilterOperation(query.ToString()); - return new FilterQuery(propertyName, filterValue, operation); - } } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs index 7286a94fc5..f9969841ee 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs @@ -1,9 +1,10 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Internal.Query; using JsonApiDotNetCore.Models; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; @@ -34,7 +35,7 @@ public SparseFieldSetTests(TestFixture fixture) public async Task Can_Select_Sparse_Fieldsets() { // arrange - var fields = new List { "Id", "Description", "CreatedDate", "AchievedDate" }; + var fields = new List { new QueryAttribute("Id"), new QueryAttribute("Description"), new QueryAttribute("CreatedDate"), new QueryAttribute("AchievedDate") }; var todoItem = new TodoItem { Description = "description", Ordinal = 1, @@ -96,5 +97,40 @@ public async Task Fields_Query_Selects_Sparse_Field_Sets() Assert.Equal(todoItem.Description, deserializeBody.Data.Attributes["description"]); Assert.Equal(todoItem.CreatedDate.ToString("G"), ((DateTime)deserializeBody.Data.Attributes["created-date"]).ToString("G")); } + + //[Fact] + //public async Task Fields_Query_Selects_Sparse_Nested_Field_Sets() + //{ + // // arrange + // var todoItem = new TodoItem + // { + // Description = "description", + // Ordinal = 1, + // CreatedDate = DateTime.Now, + // Owner = new Person() { Age = 30, FirstName = "Jack", LastName = "Daniels"} + // }; + // _dbContext.TodoItems.Add(todoItem); + // await _dbContext.SaveChangesAsync(); + + // var builder = new WebHostBuilder() + // .UseStartup(); + // var httpMethod = new HttpMethod("GET"); + // var server = new TestServer(builder); + // var client = server.CreateClient(); + + // var route = $"/api/v1/todo-items/{todoItem.Id}?include=owner&fields[todo-items]=owner.age"; + // var request = new HttpRequestMessage(httpMethod, route); + + // // act + // var response = await client.SendAsync(request); + // var body = await response.Content.ReadAsStringAsync(); + // var deserializeBody = JsonConvert.DeserializeObject(body); + // var included = deserializeBody.Included.First(); + // // assert + // Assert.Equal(todoItem.StringId, deserializeBody.Data.Id); + // Assert.Empty(deserializeBody.Data.Attributes); + // Assert.Single(included.Attributes); + // Assert.Equal(todoItem.Owner.Age, included.Attributes["age"]); + //} } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs index ccc9a1e870..01c24220ca 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs @@ -25,6 +25,7 @@ public class TodoItemControllerTests private AppDbContext _context; private IJsonApiContext _jsonApiContext; private Faker _todoItemFaker; + private Faker _personFaker; public TodoItemControllerTests(TestFixture fixture) { @@ -35,6 +36,11 @@ public TodoItemControllerTests(TestFixture fixture) .RuleFor(t => t.Description, f => f.Lorem.Sentence()) .RuleFor(t => t.Ordinal, f => f.Random.Number()) .RuleFor(t => t.CreatedDate, f => f.Date.Past()); + + _personFaker = new Faker() + .RuleFor(t => t.FirstName, f => f.Name.FirstName()) + .RuleFor(t => t.LastName, f => f.Name.LastName()) + .RuleFor(t => t.Age, f => f.Random.Int(1, 99)); } [Fact] @@ -217,6 +223,82 @@ public async Task Can_Sort_TodoItems_By_Ordinal_Ascending() } } + [Fact] + public async Task Can_Sort_TodoItems_By_Nested_Attribute_Ascending() + { + // Arrange + _context.TodoItems.RemoveRange(_context.TodoItems); + + const int numberOfItems = 5; + + for (var i = 1; i < numberOfItems; i++) + { + var todoItem = _todoItemFaker.Generate(); + todoItem.Ordinal = i; + todoItem.Owner = _personFaker.Generate(); + _context.TodoItems.Add(todoItem); + } + _context.SaveChanges(); + + var httpMethod = new HttpMethod("GET"); + var route = $"/api/v1/todo-items?include=owner&sort=owner.age"; + var request = new HttpRequestMessage(httpMethod, route); + + // Act + var response = await _fixture.Client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + var deserializedBody = _fixture.GetService().DeserializeList(body); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotEmpty(deserializedBody); + + long lastAge = 0; + foreach (var todoItemResult in deserializedBody) + { + Assert.True(todoItemResult.Owner.Age > lastAge); + lastAge = todoItemResult.Owner.Age; + } + } + + [Fact] + public async Task Can_Sort_TodoItems_By_Nested_Attribute_Descending() + { + // Arrange + _context.TodoItems.RemoveRange(_context.TodoItems); + + const int numberOfItems = 5; + + for (var i = 1; i < numberOfItems; i++) + { + var todoItem = _todoItemFaker.Generate(); + todoItem.Ordinal = i; + todoItem.Owner = _personFaker.Generate(); + _context.TodoItems.Add(todoItem); + } + _context.SaveChanges(); + + var httpMethod = new HttpMethod("GET"); + var route = $"/api/v1/todo-items?include=owner&sort=-owner.age"; + var request = new HttpRequestMessage(httpMethod, route); + + // Act + var response = await _fixture.Client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + var deserializedBody = _fixture.GetService().DeserializeList(body); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotEmpty(deserializedBody); + + int maxAge = deserializedBody.Max(i => i.Owner.Age) + 1; + foreach (var todoItemResult in deserializedBody) + { + Assert.True(todoItemResult.Owner.Age < maxAge); + maxAge = todoItemResult.Owner.Age; + } + } + [Fact] public async Task Can_Sort_TodoItems_By_Ordinal_Descending() { diff --git a/test/UnitTests/Services/QueryParser_Tests.cs b/test/UnitTests/Services/QueryParser_Tests.cs index 64c9830f2b..5cf5a34cc0 100644 --- a/test/UnitTests/Services/QueryParser_Tests.cs +++ b/test/UnitTests/Services/QueryParser_Tests.cs @@ -263,8 +263,8 @@ public void Can_Parse_Fields_Query() // assert Assert.NotEmpty(querySet.Fields); Assert.Equal(2, querySet.Fields.Count); - Assert.Equal("Id", querySet.Fields[0]); - Assert.Equal(internalAttrName, querySet.Fields[1]); + Assert.Equal("Id", querySet.Fields.ElementAt(0).Attribute); + Assert.Equal(internalAttrName, querySet.Fields.ElementAt(1).Attribute); } [Fact] @@ -355,4 +355,4 @@ public void Can_Parse_Page_Number_Query(string value, int expectedValue, bool sh } } } -} \ No newline at end of file +} From 8d46f523cccdf540fb615baa9328e62adadda3ef Mon Sep 17 00:00:00 2001 From: Milos Date: Wed, 26 Sep 2018 01:27:53 +0200 Subject: [PATCH 02/18] Requested changes --- .../Builders/DocumentBuilder.cs | 2 +- .../Extensions/IQueryableExtensions.cs | 62 ++++--------- .../Internal/Query/AttrFilterQuery.cs | 4 +- .../Internal/Query/FilterOperations.cs | 30 +------ .../Internal/Query/FilterOperationsHelper.cs | 66 ++++++++++++++ .../Internal/Query/FilterQuery.cs | 16 +++- .../Internal/Query/QueryAttribute.cs | 1 - .../Internal/Query/QuerySet.cs | 2 +- .../Internal/Query/RelatedAttrFilterQuery.cs | 4 +- src/JsonApiDotNetCore/Services/QueryParser.cs | 89 +++++-------------- .../Acceptance/Spec/SparseFieldSetTests.cs | 37 +------- .../Acceptance/TodoItemsControllerTests.cs | 8 +- test/UnitTests/Services/QueryParser_Tests.cs | 6 +- 13 files changed, 138 insertions(+), 189 deletions(-) create mode 100644 src/JsonApiDotNetCore/Internal/Query/FilterOperationsHelper.cs diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index cfbd6c21e9..6afa13e029 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -142,7 +142,7 @@ private bool ShouldIncludeAttribute(AttrAttribute attr, object attributeValue) return OmitNullValuedAttribute(attr, attributeValue) == false && ((_jsonApiContext.QuerySet == null || _jsonApiContext.QuerySet.Fields.Count == 0) - || _jsonApiContext.QuerySet.Fields.Any(i => i.Attribute == attr.InternalAttributeName)); + || _jsonApiContext.QuerySet.Fields.Contains(attr.InternalAttributeName)); } private bool OmitNullValuedAttribute(AttrAttribute attr, object attributeValue) diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index 11e206e1e3..97814ad9c9 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -171,14 +171,14 @@ public static IQueryable Filter(this IQueryable sourc try { - if (op == FilterOperationsEnum.@in || op == FilterOperationsEnum.nin) + if (op == FilterOperations.@in || op == FilterOperations.nin) { string[] propertyValues = filterQuery.PropertyValue.Split(','); var lambdaIn = ArrayContainsPredicate(propertyValues, property.Name, op); return source.Where(lambdaIn); } - else if (op == FilterOperationsEnum.isnotnull || op == FilterOperationsEnum.isnull) { + else if (op == FilterOperations.isnotnull || op == FilterOperations.isnull) { // {model} var parameter = Expression.Parameter(concreteType, "model"); // {model.Id} @@ -231,7 +231,7 @@ public static IQueryable Filter(this IQueryable sourc try { - if (filterQuery.FilterOperation == FilterOperationsEnum.@in || filterQuery.FilterOperation == FilterOperationsEnum.nin) + if (filterQuery.FilterOperation == FilterOperations.@in || filterQuery.FilterOperation == FilterOperations.nin) { string[] propertyValues = filterQuery.PropertyValue.Split(','); var lambdaIn = ArrayContainsPredicate(propertyValues, relatedAttr.Name, filterQuery.FilterOperation, relation.Name); @@ -271,43 +271,43 @@ public static IQueryable Filter(this IQueryable sourc private static bool IsNullable(Type type) => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); - private static Expression GetFilterExpressionLambda(Expression left, Expression right, FilterOperationsEnum operation) + private static Expression GetFilterExpressionLambda(Expression left, Expression right, FilterOperations operation) { Expression body; switch (operation) { - case FilterOperationsEnum.eq: + case FilterOperations.eq: // {model.Id == 1} body = Expression.Equal(left, right); break; - case FilterOperationsEnum.lt: + case FilterOperations.lt: // {model.Id < 1} body = Expression.LessThan(left, right); break; - case FilterOperationsEnum.gt: + case FilterOperations.gt: // {model.Id > 1} body = Expression.GreaterThan(left, right); break; - case FilterOperationsEnum.le: + case FilterOperations.le: // {model.Id <= 1} body = Expression.LessThanOrEqual(left, right); break; - case FilterOperationsEnum.ge: + case FilterOperations.ge: // {model.Id >= 1} body = Expression.GreaterThanOrEqual(left, right); break; - case FilterOperationsEnum.like: + case FilterOperations.like: body = Expression.Call(left, "Contains", null, right); break; // {model.Id != 1} - case FilterOperationsEnum.ne: + case FilterOperations.ne: body = Expression.NotEqual(left, right); break; - case FilterOperationsEnum.isnotnull: + case FilterOperations.isnotnull: // {model.Id != null} body = Expression.NotEqual(left, right); break; - case FilterOperationsEnum.isnull: + case FilterOperations.isnull: // {model.Id == null} body = Expression.Equal(left, right); break; @@ -318,7 +318,7 @@ private static Expression GetFilterExpressionLambda(Expression left, Expression return body; } - private static Expression> ArrayContainsPredicate(string[] propertyValues, string fieldname, FilterOperationsEnum op, string relationName = null) + private static Expression> ArrayContainsPredicate(string[] propertyValues, string fieldname, FilterOperations op, string relationName = null) { ParameterExpression entity = Expression.Parameter(typeof(TSource), "entity"); MemberExpression member; @@ -333,7 +333,7 @@ private static Expression> ArrayContainsPredicate(s var method = ContainsMethod.MakeGenericMethod(member.Type); var obj = TypeHelper.ConvertListType(propertyValues, member.Type); - if (op == FilterOperationsEnum.@in) + if (op == FilterOperations.@in) { // Where(i => arr.Contains(i.column)) var contains = Expression.Call(method, new Expression[] { Expression.Constant(obj), member }); @@ -347,44 +347,20 @@ private static Expression> ArrayContainsPredicate(s } } - public static IQueryable Select(this IQueryable source, List columns) + public static IQueryable Select(this IQueryable source, List 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 attrs = new List(); - // Key = Relationship, Value = Attribute - var relationAttrs = new Dictionary(); - foreach(var item in columns) - { - if (item.IsAttributeOfRelationship) - relationAttrs.Add(item.RelationshipAttribute, item.Attribute) ; - else - attrs.Add(item.Attribute); - } - - var bindings = new List(); - bindings.AddRange(attrs.Select(column => Expression.Bind(resultType.GetProperty(column), Expression.PropertyOrField(parameter, column)))); - - - //foreach (var relationAttr in relationAttrs) - //{ - // var relation = Expression.PropertyOrField(parameter, relationAttr.Key); - // var member = Expression.Property(relation, relationAttr.Value); - // var relationType = member.Type; - - // var relationshipBindings = new List(); - // relationshipBindings.AddRange(attrs.Select(column => Expression.Bind(resultType.GetProperty(column), Expression.PropertyOrField(parameter, column)))); - // var body = Expression.MemberInit(Expression.New(relation.Type), bindings); - // var ah = Expression.Bind(relation.Member, member.Expression); - // bindings.Add(ah); - //} + 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); diff --git a/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs index d1d7d1d32d..9d429d76ed 100644 --- a/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs @@ -15,11 +15,11 @@ public AttrFilterQuery( throw new JsonApiException(400, $"Filter is not allowed for attribute '{Attribute.PublicAttributeName}'."); PropertyValue = filterQuery.Value; - FilterOperation = FilterOperations.GetFilterOperation(filterQuery.Operation); + FilterOperation = filterQuery.OperationType; } public string PropertyValue { get; } - public FilterOperationsEnum FilterOperation { get; } + public FilterOperations FilterOperation { get; } } } diff --git a/src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs b/src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs index b4171284d3..9ed1f4e99d 100644 --- a/src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs +++ b/src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs @@ -3,7 +3,7 @@ namespace JsonApiDotNetCore.Internal.Query { - public enum FilterOperationsEnum + public enum FilterOperations { eq = 0, lt = 1, @@ -18,32 +18,4 @@ public enum FilterOperationsEnum isnotnull = 10 } - public class FilterOperations - { - public static FilterOperationsEnum GetFilterOperation(string prefix) - { - if (prefix.Length == 0) return FilterOperationsEnum.eq; - - if (Enum.TryParse(prefix, out FilterOperationsEnum opertion) == false) - throw new JsonApiException(400, $"Invalid filter prefix '{prefix}'"); - - return opertion; - } - - public static string GetFilterOperationFromQuery(string query) - { - var values = query.Split(QueryConstants.COLON); - - if (values.Length == 1) - return string.Empty; - - var operation = values[0]; - // remove prefix from value - if (Enum.TryParse(operation, out FilterOperationsEnum op) == false) - return string.Empty; - - return operation; - } - - } } diff --git a/src/JsonApiDotNetCore/Internal/Query/FilterOperationsHelper.cs b/src/JsonApiDotNetCore/Internal/Query/FilterOperationsHelper.cs new file mode 100644 index 0000000000..ba5eee3f72 --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Query/FilterOperationsHelper.cs @@ -0,0 +1,66 @@ +// ReSharper disable InconsistentNaming +using System; +using System.Linq; + +namespace JsonApiDotNetCore.Internal.Query +{ + public class FilterOperationsHelper + { + /// + /// Get filter operation enum and value by string value. + /// Input string can contain: + /// a) property value only, then FilterOperations.eq, value is returned + /// b) filter prefix and value e.g. "prefix:value", then FilterOperations.prefix, value is returned + /// In case of prefix is provided and is not in FilterOperations enum, + /// the invalid filter prefix exception is thrown. + /// + /// + /// + public static (FilterOperations opereation,string value) GetFilterOperationAndValue(string input) + { + // value is empty + if (input.Length == 0) + return (FilterOperations.eq, input); + + // split value + var values = input.Split(QueryConstants.COLON); + // value only + if(values.Length == 1) + return (FilterOperations.eq, input); + // prefix:value + else if (values.Length == 2) + { + var (operation, succeeded) = ParseFilterOperation(values[0]); + if (succeeded == false) + throw new JsonApiException(400, $"Invalid filter prefix '{values[0]}'"); + + return (operation, values[1]); + } + // some:colon:value OR prefix:some:colon:value (datetime) + else + { + // succeeded = false means no prefix found => some value with colons(datetime) + // succeeded = true means prefix provide + some value with colons(datetime) + var (operation, succeeded) = ParseFilterOperation(values[0]); + var value = ""; + // datetime + if(succeeded == false) + value = string.Join(QueryConstants.COLON_STR, values); + else + value = string.Join(QueryConstants.COLON_STR, values.Skip(1)); + return (operation, value); + } + } + + /// + /// Returns typed operation result and info about parsing success + /// + /// String represented operation + /// + public static (FilterOperations operation, bool succeeded) ParseFilterOperation(string operation) + { + var success = Enum.TryParse(operation, out FilterOperations opertion); + return (opertion, success); + } + } +} diff --git a/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs index 852af1e55e..d71c638b6c 100644 --- a/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs @@ -5,18 +5,32 @@ namespace JsonApiDotNetCore.Internal.Query { public class FilterQuery: QueryAttribute { + [Obsolete("You should use constructor with strongly typed OperationType.")] public FilterQuery(string attribute, string value, string operation) :base(attribute) { Key = attribute.ToProperCase(); Value = value; Operation = operation; + + Enum.TryParse(operation, out FilterOperations opertion); + OperationType = opertion; + } + + public FilterQuery(string attribute, string value, FilterOperations operationType) + : base(attribute) + { + Value = value; + OperationType = operationType; + Operation = operationType.ToString(); } - + [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 Value { get; set; } + [Obsolete("Operation has been replaced by '" + nameof(OperationType) + "'. OperationType is typed enum value for Operation property. This should be default property for providing operation type, because of unsustainable string (not typed) value.")] public string Operation { get; set; } + public FilterOperations OperationType { get; set; } } } diff --git a/src/JsonApiDotNetCore/Internal/Query/QueryAttribute.cs b/src/JsonApiDotNetCore/Internal/Query/QueryAttribute.cs index 3b6cc0a099..0f7d7bc138 100644 --- a/src/JsonApiDotNetCore/Internal/Query/QueryAttribute.cs +++ b/src/JsonApiDotNetCore/Internal/Query/QueryAttribute.cs @@ -16,7 +16,6 @@ public QueryAttribute(string attribute) Attribute = attribute; IsAttributeOfRelationship = false; } - } public string Attribute { get; } diff --git a/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs b/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs index de89c0cdf6..1a60879667 100644 --- a/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs +++ b/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs @@ -8,6 +8,6 @@ public class QuerySet public PageQuery PageQuery { get; set; } = new PageQuery(); public List SortParameters { get; set; } = new List(); public List IncludedRelationships { get; set; } = new List(); - public List Fields { get; set; } = new List(); + public List Fields { get; set; } = new List(); } } diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs index 67ce619a9b..efa9052ff9 100644 --- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs @@ -13,11 +13,11 @@ public RelatedAttrFilterQuery( :base(jsonApiContext, filterQuery) { PropertyValue = filterQuery.Value; - FilterOperation = FilterOperations.GetFilterOperation(filterQuery.Operation); + FilterOperation = filterQuery.OperationType; } public string PropertyValue { get; set; } - public FilterOperationsEnum FilterOperation { get; set; } + public FilterOperations FilterOperation { get; set; } } } diff --git a/src/JsonApiDotNetCore/Services/QueryParser.cs b/src/JsonApiDotNetCore/Services/QueryParser.cs index 93e2d60d07..e671e1c2d7 100644 --- a/src/JsonApiDotNetCore/Services/QueryParser.cs +++ b/src/JsonApiDotNetCore/Services/QueryParser.cs @@ -85,41 +85,37 @@ protected virtual List ParseFilterQuery(string key, string value) var propertyName = key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1]; // InArray case - string op = FilterOperations.GetFilterOperationFromQuery(value); - if (string.Equals(op, FilterOperationsEnum.@in.ToString(), StringComparison.OrdinalIgnoreCase) - || string.Equals(op, FilterOperationsEnum.nin.ToString(), StringComparison.OrdinalIgnoreCase)) - { - (var operation, var filterValue) = ParseFilterOperation(value); - queries.Add(new FilterQuery(propertyName, filterValue, op)); - } + var arrOpVal = FilterOperationsHelper.GetFilterOperationAndValue(value); + if (arrOpVal.opereation == FilterOperations.@in || arrOpVal.opereation == FilterOperations.nin) + queries.Add(new FilterQuery(propertyName, arrOpVal.value, arrOpVal.opereation)); else { var values = value.Split(QueryConstants.COMMA); foreach (var val in values) { - (var operation, var filterValue) = ParseFilterOperation(val); - queries.Add(new FilterQuery(propertyName, filterValue, operation)); + var opVal = FilterOperationsHelper.GetFilterOperationAndValue(val); + queries.Add(new FilterQuery(propertyName, opVal.value, opVal.opereation)); } } return queries; } - protected virtual (string operation, string value) ParseFilterOperation(string value) - { - if (value.Length < 3) - return (string.Empty, value); + //protected virtual (string operation, string value) ParseFilterOperation(string value) + //{ + // if (value.Length < 3) + // return (string.Empty, value); - var operation = FilterOperations.GetFilterOperationFromQuery(value); - var values = value.Split(QueryConstants.COLON); + // var operation = FilterOperationsHelper.GetFilterOperation(value); + // var values = value.Split(QueryConstants.COLON); - if (string.IsNullOrEmpty(operation)) - return (string.Empty, value); + // if (string.IsNullOrEmpty(operation)) + // return (string.Empty, value); - value = string.Join(QueryConstants.COLON_STR, values.Skip(1)); + // value = string.Join(QueryConstants.COLON_STR, values.Skip(1)); - return (operation, value); - } + // return (operation, value); + //} protected virtual PageQuery ParsePageQuery(PageQuery pageQuery, string key, string value) { @@ -156,7 +152,6 @@ protected virtual List ParseSortParameters(string value) foreach (var sortSegment in sortSegments) { - var propertyName = sortSegment; var direction = SortDirection.Ascending; @@ -179,11 +174,13 @@ protected virtual List ParseIncludedRelationships(string value) .ToList(); } - protected virtual List ParseFieldsQuery(string key, string value) + protected virtual List ParseFieldsQuery(string key, string value) { // expected: fields[TYPE]=prop1,prop2 var typeName = key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1]; - var includedFields = new List { new QueryAttribute(nameof(Identifiable.Id)) }; + + const string ID = "Id"; + var includedFields = new List { ID }; // this will not support nested inclusions, it requires that the typeName is the current request type if (string.Equals(typeName, _controllerContext.RequestEntity.EntityName, StringComparison.OrdinalIgnoreCase) == false) @@ -192,53 +189,13 @@ protected virtual List ParseFieldsQuery(string key, string value var fields = value.Split(QueryConstants.COMMA); foreach (var field in fields) { - string queryField = null; - if(field.Contains('.')) - { - var properties = field.Split('.'); - var relationship = GetRelationshipAttribute(properties[0]); - - // Temp method - no chance to get attribute of relationship on Controller level - queryField = relationship.InternalRelationshipName + "." + UppercaseFirst(properties[1]); - } - else - { - var attr = GetAttribute(field); - queryField = attr.InternalAttributeName; - } - - // Store Internal attributes names - includedFields.Add(new QueryAttribute(queryField)); + var attr = GetAttribute(field); + var internalAttrName = attr.InternalAttributeName; + includedFields.Add(internalAttrName); } return includedFields; } - - private string UppercaseFirst(string s) - { - // Check for empty string. - if (string.IsNullOrEmpty(s)) - { - return string.Empty; - } - // Return char and concat substring. - return char.ToUpper(s[0]) + s.Substring(1); - } - - protected virtual RelationshipAttribute GetRelationshipAttribute(string relationship) - { - try - { - return _controllerContext - .RequestEntity - .Relationships - .Single(attr => attr.Is(relationship)); - } - catch (InvalidOperationException e) - { - throw new JsonApiException(400, $"Relationship '{relationship}' does not exist on resource '{_controllerContext.RequestEntity.EntityName}'", e); - } - } protected virtual AttrAttribute GetAttribute(string attribute) { diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs index f9969841ee..cb65191ef1 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs @@ -35,7 +35,7 @@ public SparseFieldSetTests(TestFixture fixture) public async Task Can_Select_Sparse_Fieldsets() { // arrange - var fields = new List { new QueryAttribute("Id"), new QueryAttribute("Description"), new QueryAttribute("CreatedDate"), new QueryAttribute("AchievedDate") }; + var fields = new List { "Id","Description", "CreatedDate", "AchievedDate" }; var todoItem = new TodoItem { Description = "description", Ordinal = 1, @@ -97,40 +97,5 @@ public async Task Fields_Query_Selects_Sparse_Field_Sets() Assert.Equal(todoItem.Description, deserializeBody.Data.Attributes["description"]); Assert.Equal(todoItem.CreatedDate.ToString("G"), ((DateTime)deserializeBody.Data.Attributes["created-date"]).ToString("G")); } - - //[Fact] - //public async Task Fields_Query_Selects_Sparse_Nested_Field_Sets() - //{ - // // arrange - // var todoItem = new TodoItem - // { - // Description = "description", - // Ordinal = 1, - // CreatedDate = DateTime.Now, - // Owner = new Person() { Age = 30, FirstName = "Jack", LastName = "Daniels"} - // }; - // _dbContext.TodoItems.Add(todoItem); - // await _dbContext.SaveChangesAsync(); - - // var builder = new WebHostBuilder() - // .UseStartup(); - // var httpMethod = new HttpMethod("GET"); - // var server = new TestServer(builder); - // var client = server.CreateClient(); - - // var route = $"/api/v1/todo-items/{todoItem.Id}?include=owner&fields[todo-items]=owner.age"; - // var request = new HttpRequestMessage(httpMethod, route); - - // // act - // var response = await client.SendAsync(request); - // var body = await response.Content.ReadAsStringAsync(); - // var deserializeBody = JsonConvert.DeserializeObject(body); - // var included = deserializeBody.Included.First(); - // // assert - // Assert.Equal(todoItem.StringId, deserializeBody.Data.Id); - // Assert.Empty(deserializeBody.Data.Attributes); - // Assert.Single(included.Attributes); - // Assert.Equal(todoItem.Owner.Age, included.Attributes["age"]); - //} } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs index 01c24220ca..9de6e6fb07 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs @@ -246,11 +246,11 @@ public async Task Can_Sort_TodoItems_By_Nested_Attribute_Ascending() // Act var response = await _fixture.Client.SendAsync(request); - var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.GetService().DeserializeList(body); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var deserializedBody = _fixture.GetService().DeserializeList(body); Assert.NotEmpty(deserializedBody); long lastAge = 0; @@ -284,11 +284,11 @@ public async Task Can_Sort_TodoItems_By_Nested_Attribute_Descending() // Act var response = await _fixture.Client.SendAsync(request); - var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture.GetService().DeserializeList(body); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var deserializedBody = _fixture.GetService().DeserializeList(body); Assert.NotEmpty(deserializedBody); int maxAge = deserializedBody.Max(i => i.Owner.Age) + 1; diff --git a/test/UnitTests/Services/QueryParser_Tests.cs b/test/UnitTests/Services/QueryParser_Tests.cs index 5cf5a34cc0..be6b8c5020 100644 --- a/test/UnitTests/Services/QueryParser_Tests.cs +++ b/test/UnitTests/Services/QueryParser_Tests.cs @@ -99,7 +99,7 @@ public void Filters_Properly_Parses_DateTime_Without_Operation() // assert Assert.Equal(dt, querySet.Filters.Single(f => f.Attribute == "key").Value); - Assert.Equal(string.Empty, querySet.Filters.Single(f => f.Attribute == "key").Operation); + Assert.Equal("eq", querySet.Filters.Single(f => f.Attribute == "key").Operation); } [Fact] @@ -263,8 +263,8 @@ public void Can_Parse_Fields_Query() // assert Assert.NotEmpty(querySet.Fields); Assert.Equal(2, querySet.Fields.Count); - Assert.Equal("Id", querySet.Fields.ElementAt(0).Attribute); - Assert.Equal(internalAttrName, querySet.Fields.ElementAt(1).Attribute); + Assert.Equal("Id", querySet.Fields[0]); + Assert.Equal(internalAttrName, querySet.Fields[1]); } [Fact] From 2f3e5d12d1e9780cd3eb2a36cdfaba1bab381d73 Mon Sep 17 00:00:00 2001 From: Milos Date: Wed, 26 Sep 2018 08:45:22 +0200 Subject: [PATCH 03/18] AttrQuery and RelatedAttrQuery constructor overload. --- .../Extensions/IQueryableExtensions.cs | 16 ++++----- .../Internal/Query/AttrFilterQuery.cs | 25 ------------- .../Internal/Query/AttrQuery.cs | 36 +++++++++++++++++-- .../Internal/Query/AttrSortQuery.cs | 23 ------------ .../Internal/Query/RelatedAttrFilterQuery.cs | 23 ------------ .../Internal/Query/RelatedAttrQuery.cs | 33 ++++++++++++++--- .../Internal/Query/RelatedAttrSortQuery.cs | 24 ------------- 7 files changed, 70 insertions(+), 110 deletions(-) delete mode 100644 src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs delete mode 100644 src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs delete mode 100644 src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs delete mode 100644 src/JsonApiDotNetCore/Internal/Query/RelatedAttrSortQuery.cs diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index 97814ad9c9..cfce520255 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -49,14 +49,14 @@ public static IOrderedQueryable Sort(this IQueryable { if (sortQuery.IsAttributeOfRelationship) { - var relatedAttrQuery = new RelatedAttrSortQuery(jsonApiContext, sortQuery); + var relatedAttrQuery = new RelatedAttrQuery(jsonApiContext, sortQuery); return sortQuery.Direction == SortDirection.Descending ? source.OrderByDescending(relatedAttrQuery) : source.OrderBy(relatedAttrQuery); } else { - var attrQuery = new AttrSortQuery(jsonApiContext, sortQuery); + var attrQuery = new AttrQuery(jsonApiContext, sortQuery); return sortQuery.Direction == SortDirection.Descending ? source.OrderByDescending(attrQuery) : source.OrderBy(attrQuery); @@ -67,14 +67,14 @@ public static IOrderedQueryable Sort(this IOrderedQueryable Filter(this IQueryable sourc return source; if (filterQuery.IsAttributeOfRelationship) - return source.Filter(new RelatedAttrFilterQuery(jsonApiContext, filterQuery)); + return source.Filter(new RelatedAttrQuery(jsonApiContext, filterQuery)); - return source.Filter(new AttrFilterQuery(jsonApiContext, filterQuery)); + return source.Filter(new AttrQuery(jsonApiContext, filterQuery)); } - public static IQueryable Filter(this IQueryable source, AttrFilterQuery filterQuery) + public static IQueryable Filter(this IQueryable source, AttrQuery filterQuery) { if (filterQuery == null) return source; @@ -214,7 +214,7 @@ public static IQueryable Filter(this IQueryable sourc } } - public static IQueryable Filter(this IQueryable source, RelatedAttrFilterQuery filterQuery) + public static IQueryable Filter(this IQueryable source, RelatedAttrQuery filterQuery) { if (filterQuery == null) return source; diff --git a/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs deleted file mode 100644 index 9d429d76ed..0000000000 --- a/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; - -namespace JsonApiDotNetCore.Internal.Query -{ - public class AttrFilterQuery : AttrQuery - { - public AttrFilterQuery( - IJsonApiContext jsonApiContext, - FilterQuery filterQuery) - :base(jsonApiContext, filterQuery) - { - if (Attribute.IsFilterable == false) - throw new JsonApiException(400, $"Filter is not allowed for attribute '{Attribute.PublicAttributeName}'."); - - PropertyValue = filterQuery.Value; - FilterOperation = filterQuery.OperationType; - } - - public string PropertyValue { get; } - public FilterOperations FilterOperation { get; } - - } -} diff --git a/src/JsonApiDotNetCore/Internal/Query/AttrQuery.cs b/src/JsonApiDotNetCore/Internal/Query/AttrQuery.cs index 8ebde76db8..bdc09fb8cd 100644 --- a/src/JsonApiDotNetCore/Internal/Query/AttrQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/AttrQuery.cs @@ -8,14 +8,46 @@ namespace JsonApiDotNetCore.Internal.Query public class AttrQuery { private readonly IJsonApiContext _jsonApiContext; + public AttrAttribute Attribute { get; } + + // Filter properties + public string PropertyValue { get; } + public FilterOperations FilterOperation { get; } + // Sort properties + public SortDirection Direction { get; set; } - public AttrQuery(IJsonApiContext jsonApiContext, QueryAttribute query) + /// + /// Build AttrQuery base on FilterQuery values. + /// + /// + /// + public AttrQuery(IJsonApiContext jsonApiContext, FilterQuery query) { _jsonApiContext = jsonApiContext; Attribute = GetAttribute(query.Attribute); + + if (Attribute.IsFilterable == false) + throw new JsonApiException(400, $"Filter is not allowed for attribute '{Attribute.PublicAttributeName}'."); + + PropertyValue = query.Value; + FilterOperation = query.OperationType; } - public AttrAttribute Attribute { get; } + /// + /// Build AttrQuery base on SortQuery values. + /// + /// + /// + public AttrQuery(IJsonApiContext jsonApiContext, SortQuery sortQuery) + { + _jsonApiContext = jsonApiContext; + Attribute = GetAttribute(sortQuery.Attribute); + + if (Attribute.IsSortable == false) + throw new JsonApiException(400, $"Sort is not allowed for attribute '{Attribute.PublicAttributeName}'."); + + Direction = sortQuery.Direction; + } private AttrAttribute GetAttribute(string attribute) { diff --git a/src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs b/src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs deleted file mode 100644 index 19f76a95e9..0000000000 --- a/src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Linq; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; - -namespace JsonApiDotNetCore.Internal.Query -{ - public class AttrSortQuery : AttrQuery - { - public AttrSortQuery( - IJsonApiContext jsonApiContext, - SortQuery sortQuery) - :base(jsonApiContext, sortQuery) - { - if (Attribute.IsSortable == false) - throw new JsonApiException(400, $"Sort is not allowed for attribute '{Attribute.PublicAttributeName}'."); - - Direction = sortQuery.Direction; - } - - public SortDirection Direction { get; set; } - - } -} diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs deleted file mode 100644 index efa9052ff9..0000000000 --- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Linq; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; - -namespace JsonApiDotNetCore.Internal.Query -{ - public class RelatedAttrFilterQuery : RelatedAttrQuery - { - - public RelatedAttrFilterQuery( - IJsonApiContext jsonApiContext, - FilterQuery filterQuery) - :base(jsonApiContext, filterQuery) - { - PropertyValue = filterQuery.Value; - FilterOperation = filterQuery.OperationType; - } - - public string PropertyValue { get; set; } - public FilterOperations FilterOperation { get; set; } - - } -} diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrQuery.cs index dda19ff5ce..bff178d691 100644 --- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrQuery.cs @@ -8,17 +8,40 @@ namespace JsonApiDotNetCore.Internal.Query public class RelatedAttrQuery { private readonly IJsonApiContext _jsonApiContext; + public AttrAttribute Attribute { get; } + public RelationshipAttribute RelationshipAttribute { get; } + + // Filter properties + public string PropertyValue { get; } + public FilterOperations FilterOperation { get; } + // Sort properties + public SortDirection Direction { get; set; } - public RelatedAttrQuery(IJsonApiContext jsonApiContext, QueryAttribute query) + public RelatedAttrQuery(IJsonApiContext jsonApiContext, FilterQuery filterQuery) { _jsonApiContext = jsonApiContext; - RelationshipAttribute = GetRelationshipAttribute(query.RelationshipAttribute); - Attribute = GetAttribute(RelationshipAttribute, query.Attribute); + RelationshipAttribute = GetRelationshipAttribute(filterQuery.RelationshipAttribute); + Attribute = GetAttribute(RelationshipAttribute, filterQuery.Attribute); + + if (Attribute.IsFilterable == false) + throw new JsonApiException(400, $"Filter is not allowed for attribute '{Attribute.PublicAttributeName}'."); + + PropertyValue = filterQuery.Value; + FilterOperation = filterQuery.OperationType; } - public AttrAttribute Attribute { get; } - public RelationshipAttribute RelationshipAttribute { get; } + public RelatedAttrQuery(IJsonApiContext jsonApiContext, SortQuery sortQuery) + { + _jsonApiContext = jsonApiContext; + RelationshipAttribute = GetRelationshipAttribute(sortQuery.RelationshipAttribute); + Attribute = GetAttribute(RelationshipAttribute, sortQuery.Attribute); + + if (Attribute.IsSortable == false) + throw new JsonApiException(400, $"Sort is not allowed for attribute '{Attribute.PublicAttributeName}'."); + + Direction = sortQuery.Direction; + } private RelationshipAttribute GetRelationshipAttribute(string relationship) { diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrSortQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrSortQuery.cs deleted file mode 100644 index 58422acb99..0000000000 --- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrSortQuery.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Linq; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; - -namespace JsonApiDotNetCore.Internal.Query -{ - public class RelatedAttrSortQuery : RelatedAttrQuery - { - - public RelatedAttrSortQuery( - IJsonApiContext jsonApiContext, - SortQuery sortQuery) - :base(jsonApiContext, sortQuery) - { - if (Attribute.IsSortable == false) - throw new JsonApiException(400, $"Sort is not allowed for attribute '{Attribute.PublicAttributeName}' on relationship '{RelationshipAttribute.PublicRelationshipName}'."); - - Direction = sortQuery.Direction; - } - - public SortDirection Direction { get; set; } - - } -} From b04ee1faccf1389d2dabaf1e57a989beae53f8dc Mon Sep 17 00:00:00 2001 From: Milos Date: Thu, 27 Sep 2018 01:20:50 +0200 Subject: [PATCH 04/18] Dramatical reduction of repeating code blocks. --- .../Extensions/IQueryableExtensions.cs | 347 ++++++++---------- .../Internal/Query/AttrQuery.cs | 32 +- .../Internal/Query/BaseAttrQuery.cs | 25 ++ .../Internal/Query/QueryAttribute.cs | 2 +- .../Internal/Query/RelatedAttrQuery.cs | 24 +- .../Acceptance/TodoItemsControllerTests.cs | 4 +- 6 files changed, 206 insertions(+), 228 deletions(-) create mode 100644 src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index cfce520255..7551b15992 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -81,70 +81,17 @@ public static IOrderedQueryable Sort(this IOrderedQueryable OrderBy(this IQueryable source, AttrQuery attrQuery) - { - return CallGenericOrderMethod(source, attrQuery.Attribute, null, "OrderBy"); - } - public static IOrderedQueryable OrderBy(this IQueryable source, RelatedAttrQuery relatedAttrQuery) - { - return CallGenericOrderMethod(source, relatedAttrQuery.Attribute, relatedAttrQuery.RelationshipAttribute, "OrderBy"); - } - - public static IOrderedQueryable OrderByDescending(this IQueryable source, AttrQuery attrQuery) - { - return CallGenericOrderMethod(source, attrQuery.Attribute, null, "OrderByDescending"); - } - public static IOrderedQueryable OrderByDescending(this IQueryable source, RelatedAttrQuery relatedAttrQuery) - { - return CallGenericOrderMethod(source, relatedAttrQuery.Attribute, relatedAttrQuery.RelationshipAttribute, "OrderByDescending"); - } + public static IOrderedQueryable OrderBy(this IQueryable source, BaseAttrQuery baseAttrQuery) + => CallGenericOrderMethod(source, baseAttrQuery, "OrderBy"); - public static IOrderedQueryable ThenBy(this IOrderedQueryable source, AttrQuery attrQuery) - { - return CallGenericOrderMethod(source, attrQuery.Attribute, null, "ThenBy"); - } - public static IOrderedQueryable ThenBy(this IOrderedQueryable source, RelatedAttrQuery relatedAttrQuery) - { - return CallGenericOrderMethod(source, relatedAttrQuery.Attribute, relatedAttrQuery.RelationshipAttribute, "ThenBy"); - } + public static IOrderedQueryable OrderByDescending(this IQueryable source, BaseAttrQuery baseAttrQuery) + => CallGenericOrderMethod(source, baseAttrQuery, "OrderByDescending"); - public static IOrderedQueryable ThenByDescending(this IOrderedQueryable source, AttrQuery attrQuery) - { - return CallGenericOrderMethod(source, attrQuery.Attribute, null, "ThenByDescending"); - } - public static IOrderedQueryable ThenByDescending(this IOrderedQueryable source, RelatedAttrQuery relatedAttrQuery) - { - return CallGenericOrderMethod(source, relatedAttrQuery.Attribute, relatedAttrQuery.RelationshipAttribute, "ThenByDescending"); - } + public static IOrderedQueryable ThenBy(this IOrderedQueryable source, BaseAttrQuery baseAttrQuery) + => CallGenericOrderMethod(source, baseAttrQuery, "ThenBy"); - private static IOrderedQueryable CallGenericOrderMethod(IQueryable source, AttrAttribute attr, RelationshipAttribute relationAttr, string method) - { - // {x} - var parameter = Expression.Parameter(typeof(TSource), "x"); - - //var property = Expression.Property(parameter, attr.InternalAttributeName); - - MemberExpression member; - // {x.relationship.propertyName} - if (relationAttr != null) - { - var relation = Expression.PropertyOrField(parameter, relationAttr.InternalRelationshipName); - member = Expression.Property(relation, attr.InternalAttributeName); - } - // {x.propertyName} - else - member = Expression.Property(parameter, attr.InternalAttributeName); - - // {x=>x.propertyName} or {x=>x.relationship.propertyName} - var lambda = Expression.Lambda(member, parameter); - - // REFLECTION: source.OrderBy(x => x.Property) - var orderByMethod = typeof(Queryable).GetMethods().First(x => x.Name == method && x.GetParameters().Length == 2); - var orderByGeneric = orderByMethod.MakeGenericMethod(typeof(TSource), member.Type); - var result = orderByGeneric.Invoke(null, new object[] { source, lambda }); - - return (IOrderedQueryable)result; - } + public static IOrderedQueryable ThenByDescending(this IOrderedQueryable source, BaseAttrQuery baseAttrQuery) + => CallGenericOrderMethod(source, baseAttrQuery, "ThenByDescending"); public static IQueryable Filter(this IQueryable source, IJsonApiContext jsonApiContext, FilterQuery filterQuery) { @@ -157,120 +104,17 @@ public static IQueryable Filter(this IQueryable sourc return source.Filter(new AttrQuery(jsonApiContext, filterQuery)); } - public static IQueryable Filter(this IQueryable source, AttrQuery filterQuery) + public static IQueryable Filter(this IQueryable source, BaseAttrQuery filterQuery) { if (filterQuery == null) return source; - var concreteType = typeof(TSource); - var property = concreteType.GetProperty(filterQuery.Attribute.InternalAttributeName); - var op = filterQuery.FilterOperation; - - if (property == null) - throw new ArgumentException($"'{filterQuery.Attribute.InternalAttributeName}' is not a valid property of '{concreteType}'"); - - try - { - if (op == FilterOperations.@in || op == FilterOperations.nin) - { - string[] propertyValues = filterQuery.PropertyValue.Split(','); - var lambdaIn = ArrayContainsPredicate(propertyValues, property.Name, op); - - return source.Where(lambdaIn); - } - else if (op == FilterOperations.isnotnull || op == FilterOperations.isnull) { - // {model} - var parameter = Expression.Parameter(concreteType, "model"); - // {model.Id} - var left = Expression.PropertyOrField(parameter, property.Name); - var right = Expression.Constant(null); - - var body = GetFilterExpressionLambda(left, right, op); - var lambda = Expression.Lambda>(body, parameter); - - return source.Where(lambda); - } - else - { // convert the incoming value to the target value type - // "1" -> 1 - var convertedValue = TypeHelper.ConvertType(filterQuery.PropertyValue, property.PropertyType); - // {model} - var parameter = Expression.Parameter(concreteType, "model"); - // {model.Id} - var left = Expression.PropertyOrField(parameter, property.Name); - // {1} - var right = Expression.Constant(convertedValue, property.PropertyType); - - var body = GetFilterExpressionLambda(left, right, op); - - var lambda = Expression.Lambda>(body, parameter); - - return source.Where(lambda); - } - } - catch (FormatException) - { - throw new JsonApiException(400, $"Could not cast {filterQuery.PropertyValue} to {property.PropertyType.Name}"); - } - } - - public static IQueryable Filter(this IQueryable source, RelatedAttrQuery filterQuery) - { - if (filterQuery == null) - return source; - - var concreteType = typeof(TSource); - var relation = concreteType.GetProperty(filterQuery.RelationshipAttribute.InternalRelationshipName); - if (relation == null) - throw new ArgumentException($"'{filterQuery.RelationshipAttribute.InternalRelationshipName}' is not a valid relationship of '{concreteType}'"); - - var relatedType = filterQuery.RelationshipAttribute.Type; - var relatedAttr = relatedType.GetProperty(filterQuery.Attribute.InternalAttributeName); - if (relatedAttr == null) - throw new ArgumentException($"'{filterQuery.Attribute.InternalAttributeName}' is not a valid attribute of '{filterQuery.RelationshipAttribute.InternalRelationshipName}'"); - - try - { - if (filterQuery.FilterOperation == FilterOperations.@in || filterQuery.FilterOperation == FilterOperations.nin) - { - string[] propertyValues = filterQuery.PropertyValue.Split(','); - var lambdaIn = ArrayContainsPredicate(propertyValues, relatedAttr.Name, filterQuery.FilterOperation, relation.Name); - - return source.Where(lambdaIn); - } - else - { - // convert the incoming value to the target value type - // "1" -> 1 - var convertedValue = TypeHelper.ConvertType(filterQuery.PropertyValue, relatedAttr.PropertyType); - // {model} - var parameter = Expression.Parameter(concreteType, "model"); - - // {model.Relationship} - var leftRelationship = Expression.PropertyOrField(parameter, relation.Name); - - // {model.Relationship.Attr} - var left = Expression.PropertyOrField(leftRelationship, relatedAttr.Name); - - // {1} - var right = Expression.Constant(convertedValue, relatedAttr.PropertyType); - - var body = GetFilterExpressionLambda(left, right, filterQuery.FilterOperation); - - var lambda = Expression.Lambda>(body, parameter); - - return source.Where(lambda); - } - } - catch (FormatException) - { - throw new JsonApiException(400, $"Could not cast {filterQuery.PropertyValue} to {relatedAttr.PropertyType.Name}"); - } + if (filterQuery.FilterOperation == FilterOperations.@in || filterQuery.FilterOperation == FilterOperations.nin) + return CallGenericWhereContainsMethod(source,filterQuery); + else + return CallGenericWhereMethod(source, filterQuery); } - private static bool IsNullable(Type type) => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); - - private static Expression GetFilterExpressionLambda(Expression left, Expression right, FilterOperations operation) { Expression body; @@ -318,35 +162,6 @@ private static Expression GetFilterExpressionLambda(Expression left, Expression return body; } - private static Expression> ArrayContainsPredicate(string[] propertyValues, string fieldname, FilterOperations op, string relationName = null) - { - ParameterExpression entity = Expression.Parameter(typeof(TSource), "entity"); - MemberExpression member; - if (!string.IsNullOrEmpty(relationName)) - { - var relation = Expression.PropertyOrField(entity, relationName); - member = Expression.Property(relation, fieldname); - } - else - member = Expression.Property(entity, fieldname); - - var method = ContainsMethod.MakeGenericMethod(member.Type); - var obj = TypeHelper.ConvertListType(propertyValues, member.Type); - - if (op == FilterOperations.@in) - { - // Where(i => arr.Contains(i.column)) - var contains = Expression.Call(method, new Expression[] { Expression.Constant(obj), member }); - return Expression.Lambda>(contains, entity); - } - else - { - // Where(i => !arr.Contains(i.column)) - var notContains = Expression.Not(Expression.Call(method, new Expression[] { Expression.Constant(obj), member })); - return Expression.Lambda>(notContains, entity); - } - } - public static IQueryable Select(this IQueryable source, List columns) { if (columns == null || columns.Count == 0) @@ -388,5 +203,141 @@ public static IQueryable PageForward(this IQueryable source, int pageSi return source; } + + #region Generic method calls + + private static IOrderedQueryable CallGenericOrderMethod(IQueryable source, BaseAttrQuery baseAttrQuery, string method) + { + // {x} + var parameter = Expression.Parameter(typeof(TSource), "x"); + MemberExpression member; + // {x.relationship.propertyName} + if (baseAttrQuery.IsAttributeOfRelationship) + { + var relation = Expression.PropertyOrField(parameter, baseAttrQuery.RelationshipAttribute.InternalRelationshipName); + member = Expression.Property(relation, baseAttrQuery.Attribute.InternalAttributeName); + } + // {x.propertyName} + else + member = Expression.Property(parameter, baseAttrQuery.Attribute.InternalAttributeName); + + // {x=>x.propertyName} or {x=>x.relationship.propertyName} + var lambda = Expression.Lambda(member, parameter); + + // REFLECTION: source.OrderBy(x => x.Property) + var orderByMethod = typeof(Queryable).GetMethods().First(x => x.Name == method && x.GetParameters().Length == 2); + var orderByGeneric = orderByMethod.MakeGenericMethod(typeof(TSource), member.Type); + var result = orderByGeneric.Invoke(null, new object[] { source, lambda }); + + return (IOrderedQueryable)result; + } + + private static IQueryable CallGenericWhereMethod(IQueryable source, BaseAttrQuery filter) + { + var op = filter.FilterOperation; + var concreteType = typeof(TSource); + PropertyInfo relationProperty = null; + PropertyInfo property = null; + MemberExpression left; + ConstantExpression right; + + // {model} + var parameter = Expression.Parameter(concreteType, "model"); + // Is relationship attribute + if (filter.IsAttributeOfRelationship) + { + relationProperty = concreteType.GetProperty(filter.RelationshipAttribute.InternalRelationshipName); + if (relationProperty == null) + throw new ArgumentException($"'{filter.RelationshipAttribute.InternalRelationshipName}' is not a valid relationship of '{concreteType}'"); + + var relatedType = filter.RelationshipAttribute.Type; + property = relatedType.GetProperty(filter.Attribute.InternalAttributeName); + if (property == null) + throw new ArgumentException($"'{filter.Attribute.InternalAttributeName}' is not a valid attribute of '{filter.RelationshipAttribute.InternalRelationshipName}'"); + + var leftRelationship = Expression.PropertyOrField(parameter, filter.RelationshipAttribute.InternalRelationshipName); + // {model.Relationship} + left = Expression.PropertyOrField(leftRelationship, property.Name); + } + // Is standalone attribute + else + { + property = concreteType.GetProperty(filter.Attribute.InternalAttributeName); + if (property == null) + throw new ArgumentException($"'{filter.Attribute.InternalAttributeName}' is not a valid property of '{concreteType}'"); + + // {model.Id} + left = Expression.PropertyOrField(parameter, property.Name); + } + + try + { + if (op == FilterOperations.isnotnull || op == FilterOperations.isnull) + right = Expression.Constant(null); + else + { + // convert the incoming value to the target value type + // "1" -> 1 + var convertedValue = TypeHelper.ConvertType(filter.PropertyValue, property.PropertyType); + // {1} + right = Expression.Constant(convertedValue, property.PropertyType); + } + + var body = GetFilterExpressionLambda(left, right, filter.FilterOperation); + var lambda = Expression.Lambda>(body, parameter); + + return source.Where(lambda); + } + catch (FormatException) + { + throw new JsonApiException(400, $"Could not cast {filter.PropertyValue} to {property.PropertyType.Name}"); + } + } + + private static IQueryable CallGenericWhereContainsMethod(IQueryable source, BaseAttrQuery filter) + { + var concreteType = typeof(TSource); + var property = concreteType.GetProperty(filter.Attribute.InternalAttributeName); + + try + { + var propertyValues = filter.PropertyValue.Split(QueryConstants.COMMA); + ParameterExpression entity = Expression.Parameter(concreteType, "entity"); + MemberExpression member; + if (filter.IsAttributeOfRelationship) + { + var relation = Expression.PropertyOrField(entity, filter.RelationshipAttribute.InternalRelationshipName); + member = Expression.Property(relation, filter.Attribute.InternalAttributeName); + } + else + member = Expression.Property(entity, filter.Attribute.InternalAttributeName); + + var method = ContainsMethod.MakeGenericMethod(member.Type); + var obj = TypeHelper.ConvertListType(propertyValues, member.Type); + + if (filter.FilterOperation == FilterOperations.@in) + { + // Where(i => arr.Contains(i.column)) + var contains = Expression.Call(method, new Expression[] { Expression.Constant(obj), member }); + var lambda = Expression.Lambda>(contains, entity); + + return source.Where(lambda); + } + else + { + // Where(i => !arr.Contains(i.column)) + var notContains = Expression.Not(Expression.Call(method, new Expression[] { Expression.Constant(obj), member })); + var lambda = Expression.Lambda>(notContains, entity); + + return source.Where(lambda); + } + } + catch (FormatException) + { + throw new JsonApiException(400, $"Could not cast {filter.PropertyValue} to {property.PropertyType.Name}"); + } + } + + #endregion } } diff --git a/src/JsonApiDotNetCore/Internal/Query/AttrQuery.cs b/src/JsonApiDotNetCore/Internal/Query/AttrQuery.cs index bdc09fb8cd..b0b3853c01 100644 --- a/src/JsonApiDotNetCore/Internal/Query/AttrQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/AttrQuery.cs @@ -5,47 +5,43 @@ namespace JsonApiDotNetCore.Internal.Query { - public class AttrQuery + public class AttrQuery : BaseAttrQuery { private readonly IJsonApiContext _jsonApiContext; - public AttrAttribute Attribute { get; } - - // Filter properties - public string PropertyValue { get; } - public FilterOperations FilterOperation { get; } - // Sort properties - public SortDirection Direction { get; set; } + private readonly bool _isAttributeOfRelationship = false; /// - /// Build AttrQuery base on FilterQuery values. + /// Build AttrQuery based on FilterQuery values. /// /// - /// - public AttrQuery(IJsonApiContext jsonApiContext, FilterQuery query) + /// + public AttrQuery(IJsonApiContext jsonApiContext, FilterQuery filterQuery) { _jsonApiContext = jsonApiContext; - Attribute = GetAttribute(query.Attribute); - + Attribute = GetAttribute(filterQuery.Attribute); + if (Attribute.IsFilterable == false) throw new JsonApiException(400, $"Filter is not allowed for attribute '{Attribute.PublicAttributeName}'."); - PropertyValue = query.Value; - FilterOperation = query.OperationType; + IsAttributeOfRelationship = _isAttributeOfRelationship; + PropertyValue = filterQuery.Value; + FilterOperation = filterQuery.OperationType; } /// - /// Build AttrQuery base on SortQuery values. + /// Build AttrQuery based on SortQuery values. /// /// - /// + /// public AttrQuery(IJsonApiContext jsonApiContext, SortQuery sortQuery) { _jsonApiContext = jsonApiContext; Attribute = GetAttribute(sortQuery.Attribute); - + if (Attribute.IsSortable == false) throw new JsonApiException(400, $"Sort is not allowed for attribute '{Attribute.PublicAttributeName}'."); + IsAttributeOfRelationship = _isAttributeOfRelationship; Direction = sortQuery.Direction; } diff --git a/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs b/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs new file mode 100644 index 0000000000..4455c6df7c --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs @@ -0,0 +1,25 @@ +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; +using System; +using System.Linq; + +namespace JsonApiDotNetCore.Internal.Query +{ + /// + /// Abstract class to make available shared properties for AttrQuery and RelatedAttrQuery + /// It elimines boilerplate of providing specified type(AttrQuery or RelatedAttrQuery) + /// while filter and sort operations and eliminates plenty of methods to keep DRY principles + /// + public abstract class BaseAttrQuery + { + public AttrAttribute Attribute { get; protected set; } + public RelationshipAttribute RelationshipAttribute { get; protected set; } + public bool IsAttributeOfRelationship { get; protected set; } + + // Filter properties + public string PropertyValue { get; protected set; } + public FilterOperations FilterOperation { get; protected set; } + // Sort properties + public SortDirection Direction { get; protected set; } + } +} diff --git a/src/JsonApiDotNetCore/Internal/Query/QueryAttribute.cs b/src/JsonApiDotNetCore/Internal/Query/QueryAttribute.cs index 0f7d7bc138..dceba53853 100644 --- a/src/JsonApiDotNetCore/Internal/Query/QueryAttribute.cs +++ b/src/JsonApiDotNetCore/Internal/Query/QueryAttribute.cs @@ -1,6 +1,6 @@ namespace JsonApiDotNetCore.Internal.Query { - public class QueryAttribute + public abstract class QueryAttribute { public QueryAttribute(string attribute) { diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrQuery.cs index bff178d691..ef1aa0dc17 100644 --- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrQuery.cs @@ -5,18 +5,16 @@ namespace JsonApiDotNetCore.Internal.Query { - public class RelatedAttrQuery + public class RelatedAttrQuery: BaseAttrQuery { private readonly IJsonApiContext _jsonApiContext; - public AttrAttribute Attribute { get; } - public RelationshipAttribute RelationshipAttribute { get; } - - // Filter properties - public string PropertyValue { get; } - public FilterOperations FilterOperation { get; } - // Sort properties - public SortDirection Direction { get; set; } + private readonly bool _isAttributeOfRelationship = true; + /// + /// Build RelatedAttrQuery based on FilterQuery values. + /// + /// + /// public RelatedAttrQuery(IJsonApiContext jsonApiContext, FilterQuery filterQuery) { _jsonApiContext = jsonApiContext; @@ -27,19 +25,27 @@ public RelatedAttrQuery(IJsonApiContext jsonApiContext, FilterQuery filterQuery) if (Attribute.IsFilterable == false) throw new JsonApiException(400, $"Filter is not allowed for attribute '{Attribute.PublicAttributeName}'."); + IsAttributeOfRelationship = _isAttributeOfRelationship; PropertyValue = filterQuery.Value; FilterOperation = filterQuery.OperationType; } + /// + /// Build RelatedAttrQuery based on SortQuery values. + /// + /// + /// public RelatedAttrQuery(IJsonApiContext jsonApiContext, SortQuery sortQuery) { _jsonApiContext = jsonApiContext; + RelationshipAttribute = GetRelationshipAttribute(sortQuery.RelationshipAttribute); Attribute = GetAttribute(RelationshipAttribute, sortQuery.Attribute); if (Attribute.IsSortable == false) throw new JsonApiException(400, $"Sort is not allowed for attribute '{Attribute.PublicAttributeName}'."); + IsAttributeOfRelationship = _isAttributeOfRelationship; Direction = sortQuery.Direction; } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs index 9de6e6fb07..07d44a1742 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs @@ -229,7 +229,7 @@ public async Task Can_Sort_TodoItems_By_Nested_Attribute_Ascending() // Arrange _context.TodoItems.RemoveRange(_context.TodoItems); - const int numberOfItems = 5; + const int numberOfItems = 10; for (var i = 1; i < numberOfItems; i++) { @@ -267,7 +267,7 @@ public async Task Can_Sort_TodoItems_By_Nested_Attribute_Descending() // Arrange _context.TodoItems.RemoveRange(_context.TodoItems); - const int numberOfItems = 5; + const int numberOfItems = 10; for (var i = 1; i < numberOfItems; i++) { From d63ff3832004e429648506f9add975f65dab4762 Mon Sep 17 00:00:00 2001 From: Milos Date: Thu, 27 Sep 2018 01:36:11 +0200 Subject: [PATCH 05/18] Fixed tests - following item can has same age value as previsous(< was replaced by <=and vice versa) --- .../Acceptance/TodoItemsControllerTests.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs index 07d44a1742..73fa029fdb 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs @@ -231,7 +231,7 @@ public async Task Can_Sort_TodoItems_By_Nested_Attribute_Ascending() const int numberOfItems = 10; - for (var i = 1; i < numberOfItems; i++) + for (var i = 1; i <= numberOfItems; i++) { var todoItem = _todoItemFaker.Generate(); todoItem.Ordinal = i; @@ -241,7 +241,7 @@ public async Task Can_Sort_TodoItems_By_Nested_Attribute_Ascending() _context.SaveChanges(); var httpMethod = new HttpMethod("GET"); - var route = $"/api/v1/todo-items?include=owner&sort=owner.age"; + var route = $"/api/v1/todo-items?page[size]={numberOfItems}&include=owner&sort=owner.age"; var request = new HttpRequestMessage(httpMethod, route); // Act @@ -256,7 +256,7 @@ public async Task Can_Sort_TodoItems_By_Nested_Attribute_Ascending() long lastAge = 0; foreach (var todoItemResult in deserializedBody) { - Assert.True(todoItemResult.Owner.Age > lastAge); + Assert.True(todoItemResult.Owner.Age >= lastAge); lastAge = todoItemResult.Owner.Age; } } @@ -269,7 +269,7 @@ public async Task Can_Sort_TodoItems_By_Nested_Attribute_Descending() const int numberOfItems = 10; - for (var i = 1; i < numberOfItems; i++) + for (var i = 1; i <= numberOfItems; i++) { var todoItem = _todoItemFaker.Generate(); todoItem.Ordinal = i; @@ -279,7 +279,7 @@ public async Task Can_Sort_TodoItems_By_Nested_Attribute_Descending() _context.SaveChanges(); var httpMethod = new HttpMethod("GET"); - var route = $"/api/v1/todo-items?include=owner&sort=-owner.age"; + var route = $"/api/v1/todo-items?page[size]={numberOfItems}&include=owner&sort=-owner.age"; var request = new HttpRequestMessage(httpMethod, route); // Act @@ -294,7 +294,7 @@ public async Task Can_Sort_TodoItems_By_Nested_Attribute_Descending() int maxAge = deserializedBody.Max(i => i.Owner.Age) + 1; foreach (var todoItemResult in deserializedBody) { - Assert.True(todoItemResult.Owner.Age < maxAge); + Assert.True(todoItemResult.Owner.Age <= maxAge); maxAge = todoItemResult.Owner.Age; } } From 487bef9e77ba92cc24f4f9f7daf298a58875911e Mon Sep 17 00:00:00 2001 From: Milos Date: Thu, 27 Sep 2018 02:36:50 +0200 Subject: [PATCH 06/18] Sort breaking change fixed. I have to pay more attention to public methods --- .../Extensions/IQueryableExtensions.cs | 54 ++++++++++--------- .../Internal/Query/RelatedAttrQuery.cs | 12 +++++ 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index 7551b15992..15ea72979e 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -49,17 +49,19 @@ public static IOrderedQueryable Sort(this IQueryable { if (sortQuery.IsAttributeOfRelationship) { + // For now is created new instance, later resolve from cache var relatedAttrQuery = new RelatedAttrQuery(jsonApiContext, sortQuery); + var path = relatedAttrQuery.GetRelatedPropertyPath(); return sortQuery.Direction == SortDirection.Descending - ? source.OrderByDescending(relatedAttrQuery) - : source.OrderBy(relatedAttrQuery); + ? source.OrderByDescending(path) + : source.OrderBy(path); } else { var attrQuery = new AttrQuery(jsonApiContext, sortQuery); return sortQuery.Direction == SortDirection.Descending - ? source.OrderByDescending(attrQuery) - : source.OrderBy(attrQuery); + ? source.OrderByDescending(attrQuery.Attribute.InternalAttributeName) + : source.OrderBy(attrQuery.Attribute.InternalAttributeName); } } @@ -68,30 +70,31 @@ public static IOrderedQueryable Sort(this IOrderedQueryable OrderBy(this IQueryable source, BaseAttrQuery baseAttrQuery) - => CallGenericOrderMethod(source, baseAttrQuery, "OrderBy"); + public static IOrderedQueryable OrderBy(this IQueryable source, string propertyName) + => CallGenericOrderMethod(source, propertyName, "OrderBy"); - public static IOrderedQueryable OrderByDescending(this IQueryable source, BaseAttrQuery baseAttrQuery) - => CallGenericOrderMethod(source, baseAttrQuery, "OrderByDescending"); + public static IOrderedQueryable OrderByDescending(this IQueryable source, string propertyName) + => CallGenericOrderMethod(source, propertyName, "OrderByDescending"); - public static IOrderedQueryable ThenBy(this IOrderedQueryable source, BaseAttrQuery baseAttrQuery) - => CallGenericOrderMethod(source, baseAttrQuery, "ThenBy"); + public static IOrderedQueryable ThenBy(this IOrderedQueryable source, string propertyName) + => CallGenericOrderMethod(source, propertyName, "ThenBy"); - public static IOrderedQueryable ThenByDescending(this IOrderedQueryable source, BaseAttrQuery baseAttrQuery) - => CallGenericOrderMethod(source, baseAttrQuery, "ThenByDescending"); + public static IOrderedQueryable ThenByDescending(this IOrderedQueryable source, string propertyName) + => CallGenericOrderMethod(source, propertyName, "ThenByDescending"); public static IQueryable Filter(this IQueryable source, IJsonApiContext jsonApiContext, FilterQuery filterQuery) { @@ -206,21 +209,24 @@ public static IQueryable PageForward(this IQueryable source, int pageSi #region Generic method calls - private static IOrderedQueryable CallGenericOrderMethod(IQueryable source, BaseAttrQuery baseAttrQuery, string method) + private static IOrderedQueryable CallGenericOrderMethod(IQueryable source, string propertyName, string method) { // {x} var parameter = Expression.Parameter(typeof(TSource), "x"); MemberExpression member; - // {x.relationship.propertyName} - if (baseAttrQuery.IsAttributeOfRelationship) + + var values = propertyName.Split('.'); + if (values.Length > 1) { - var relation = Expression.PropertyOrField(parameter, baseAttrQuery.RelationshipAttribute.InternalRelationshipName); - member = Expression.Property(relation, baseAttrQuery.Attribute.InternalAttributeName); + var relation = Expression.PropertyOrField(parameter, values[0]); + // {x.relationship.propertyName} + member = Expression.Property(relation, values[1]); } - // {x.propertyName} else - member = Expression.Property(parameter, baseAttrQuery.Attribute.InternalAttributeName); - + { + // {x.propertyName} + member = Expression.Property(parameter, values[0]); + } // {x=>x.propertyName} or {x=>x.relationship.propertyName} var lambda = Expression.Lambda(member, parameter); diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrQuery.cs index ef1aa0dc17..57f6a05788 100644 --- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrQuery.cs @@ -49,6 +49,18 @@ public RelatedAttrQuery(IJsonApiContext jsonApiContext, SortQuery sortQuery) Direction = sortQuery.Direction; } + /// + /// Get relationship and attribute connected by '.' character + /// + /// + /// "TodoItem.Owner" + /// + /// + public string GetRelatedPropertyPath() + { + return string.Format("{0}.{1}", RelationshipAttribute.InternalRelationshipName, Attribute.InternalAttributeName); + } + private RelationshipAttribute GetRelationshipAttribute(string relationship) { try From 18014f6d30f598373c60344ec60074202bdf6994 Mon Sep 17 00:00:00 2001 From: Milos Date: Fri, 28 Sep 2018 03:08:31 +0200 Subject: [PATCH 07/18] Back to the roots. There was few breaking changes, so I have to turn back to provide compatibility. --- .../Data/DefaultEntityRepository.cs | 2 +- .../Extensions/IQueryableExtensions.cs | 93 +++----- .../Internal/Query/AttrFilterQuery.cs | 34 +++ .../Internal/Query/AttrQuery.cs | 64 ------ .../Internal/Query/BaseAttrQuery.cs | 26 ++- .../Internal/Query/BaseFilterQuery.cs | 23 ++ .../Internal/Query/FilterOperations.cs | 3 - .../Internal/Query/FilterOperationsHelper.cs | 66 ------ .../Internal/Query/FilterQuery.cs | 33 ++- .../Internal/Query/QueryAttribute.cs | 25 --- .../Internal/Query/QueryConstants.cs | 3 +- .../Internal/Query/RelatedAttrFilterQuery.cs | 45 ++++ .../Internal/Query/RelatedAttrQuery.cs | 92 -------- .../Internal/Query/SortQuery.cs | 22 +- src/JsonApiDotNetCore/Services/QueryParser.cs | 206 ++++++++++++++++-- test/UnitTests/Services/QueryParser_Tests.cs | 4 +- 16 files changed, 396 insertions(+), 345 deletions(-) create mode 100644 src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs delete mode 100644 src/JsonApiDotNetCore/Internal/Query/AttrQuery.cs create mode 100644 src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs delete mode 100644 src/JsonApiDotNetCore/Internal/Query/FilterOperationsHelper.cs delete mode 100644 src/JsonApiDotNetCore/Internal/Query/QueryAttribute.cs create mode 100644 src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs delete mode 100644 src/JsonApiDotNetCore/Internal/Query/RelatedAttrQuery.cs diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index fde424fa53..27fda5629b 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -72,7 +72,7 @@ public virtual IQueryable Filter(IQueryable entities, FilterQu /// public virtual IQueryable Sort(IQueryable entities, List sortQueries) { - return entities.Sort(_jsonApiContext, sortQueries); + return entities.Sort(sortQueries); } /// diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index 15ea72979e..51e0865856 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -29,60 +29,37 @@ private static MethodInfo ContainsMethod } } - public static IQueryable Sort(this IQueryable source, IJsonApiContext jsonApiContext, List sortQueries) + public static IQueryable Sort(this IQueryable source, List sortQueries) { if (sortQueries == null || sortQueries.Count == 0) return source; - var orderedEntities = source.Sort(jsonApiContext, sortQueries[0]); + var orderedEntities = source.Sort(sortQueries[0]); if (sortQueries.Count <= 1) return orderedEntities; for (var i = 1; i < sortQueries.Count; i++) - orderedEntities = orderedEntities.Sort(jsonApiContext, sortQueries[i]); + orderedEntities = orderedEntities.Sort(sortQueries[i]); return orderedEntities; } - public static IOrderedQueryable Sort(this IQueryable source, IJsonApiContext jsonApiContext, SortQuery sortQuery) + public static IOrderedQueryable Sort(this IQueryable source, SortQuery sortQuery) { - if (sortQuery.IsAttributeOfRelationship) - { - // For now is created new instance, later resolve from cache - var relatedAttrQuery = new RelatedAttrQuery(jsonApiContext, sortQuery); - var path = relatedAttrQuery.GetRelatedPropertyPath(); - return sortQuery.Direction == SortDirection.Descending - ? source.OrderByDescending(path) - : source.OrderBy(path); - } - else - { - var attrQuery = new AttrQuery(jsonApiContext, sortQuery); - return sortQuery.Direction == SortDirection.Descending - ? source.OrderByDescending(attrQuery.Attribute.InternalAttributeName) - : source.OrderBy(attrQuery.Attribute.InternalAttributeName); - } + var path = sortQuery.GetPropertyPath(); + return sortQuery.Direction == SortDirection.Descending + ? source.OrderByDescending(path) + : source.OrderBy(path); } - public static IOrderedQueryable Sort(this IOrderedQueryable source, IJsonApiContext jsonApiContext, SortQuery sortQuery) + public static IOrderedQueryable Sort(this IOrderedQueryable source, SortQuery sortQuery) { - if (sortQuery.IsAttributeOfRelationship) - { - var relatedAttrQuery = new RelatedAttrQuery(jsonApiContext, sortQuery); - var path = relatedAttrQuery.GetRelatedPropertyPath(); - return sortQuery.Direction == SortDirection.Descending - ? source.OrderByDescending(path) - : source.OrderBy(path); - } - else - { - var attrQuery = new AttrQuery(jsonApiContext, sortQuery); - return sortQuery.Direction == SortDirection.Descending - ? source.OrderByDescending(attrQuery.Attribute.InternalAttributeName) - : source.OrderBy(attrQuery.Attribute.InternalAttributeName); - } - } + var path = sortQuery.GetPropertyPath(); + return sortQuery.Direction == SortDirection.Descending + ? source.OrderByDescending(path) + : source.OrderBy(path); + } public static IOrderedQueryable OrderBy(this IQueryable source, string propertyName) => CallGenericOrderMethod(source, propertyName, "OrderBy"); @@ -101,13 +78,15 @@ public static IQueryable Filter(this IQueryable sourc if (filterQuery == null) return source; - if (filterQuery.IsAttributeOfRelationship) - return source.Filter(new RelatedAttrQuery(jsonApiContext, filterQuery)); + // Relationship.Attribute + if ((filterQuery.IsStringBasedInit && filterQuery.Attribute.Contains(QueryConstants.DOT)) + || filterQuery.IsAttributeOfRelationship) + return source.Filter(new RelatedAttrFilterQuery(jsonApiContext, filterQuery)); - return source.Filter(new AttrQuery(jsonApiContext, filterQuery)); + return source.Filter(new AttrFilterQuery(jsonApiContext, filterQuery)); } - public static IQueryable Filter(this IQueryable source, BaseAttrQuery filterQuery) + public static IQueryable Filter(this IQueryable source, BaseFilterQuery filterQuery) { if (filterQuery == null) return source; @@ -238,7 +217,7 @@ private static IOrderedQueryable CallGenericOrderMethod(IQuery return (IOrderedQueryable)result; } - private static IQueryable CallGenericWhereMethod(IQueryable source, BaseAttrQuery filter) + private static IQueryable CallGenericWhereMethod(IQueryable source, BaseFilterQuery filter) { var op = filter.FilterOperation; var concreteType = typeof(TSource); @@ -250,27 +229,27 @@ private static IQueryable CallGenericWhereMethod(IQueryable CallGenericWhereMethod(IQueryable CallGenericWhereContainsMethod(IQueryable source, BaseAttrQuery filter) + private static IQueryable CallGenericWhereContainsMethod(IQueryable source, BaseFilterQuery filter) { var concreteType = typeof(TSource); - var property = concreteType.GetProperty(filter.Attribute.InternalAttributeName); + var property = concreteType.GetProperty(filter.FilteredAttribute.InternalAttributeName); try { var propertyValues = filter.PropertyValue.Split(QueryConstants.COMMA); ParameterExpression entity = Expression.Parameter(concreteType, "entity"); MemberExpression member; - if (filter.IsAttributeOfRelationship) + if (filter.FilteredRelationship != null) { - var relation = Expression.PropertyOrField(entity, filter.RelationshipAttribute.InternalRelationshipName); - member = Expression.Property(relation, filter.Attribute.InternalAttributeName); + var relation = Expression.PropertyOrField(entity, filter.FilteredRelationship.InternalRelationshipName); + member = Expression.Property(relation, filter.FilteredAttribute.InternalAttributeName); } else - member = Expression.Property(entity, filter.Attribute.InternalAttributeName); + member = Expression.Property(entity, filter.FilteredAttribute.InternalAttributeName); var method = ContainsMethod.MakeGenericMethod(member.Type); var obj = TypeHelper.ConvertListType(propertyValues, member.Type); diff --git a/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs new file mode 100644 index 0000000000..6571e5b9e6 --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs @@ -0,0 +1,34 @@ +using System.Linq; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; + +namespace JsonApiDotNetCore.Internal.Query +{ + public class AttrFilterQuery : BaseFilterQuery + { + private readonly IJsonApiContext _jsonApiContext; + + public AttrFilterQuery( + IJsonApiContext jsonApiContext, + FilterQuery filterQuery) + { + _jsonApiContext = jsonApiContext; + + var attribute = GetAttribute(filterQuery.Attribute); + + 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}'."); + + FilteredAttribute = attribute; + PropertyValue = filterQuery.Value; + FilterOperation = GetFilterOperation(filterQuery.Operation); + } + + + private AttrAttribute GetAttribute(string attribute) => + _jsonApiContext.RequestEntity.Attributes.FirstOrDefault(attr => attr.Is(attribute)); + } +} diff --git a/src/JsonApiDotNetCore/Internal/Query/AttrQuery.cs b/src/JsonApiDotNetCore/Internal/Query/AttrQuery.cs deleted file mode 100644 index b0b3853c01..0000000000 --- a/src/JsonApiDotNetCore/Internal/Query/AttrQuery.cs +++ /dev/null @@ -1,64 +0,0 @@ -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; -using System; -using System.Linq; - -namespace JsonApiDotNetCore.Internal.Query -{ - public class AttrQuery : BaseAttrQuery - { - private readonly IJsonApiContext _jsonApiContext; - private readonly bool _isAttributeOfRelationship = false; - - /// - /// Build AttrQuery based on FilterQuery values. - /// - /// - /// - public AttrQuery(IJsonApiContext jsonApiContext, FilterQuery filterQuery) - { - _jsonApiContext = jsonApiContext; - Attribute = GetAttribute(filterQuery.Attribute); - - if (Attribute.IsFilterable == false) - throw new JsonApiException(400, $"Filter is not allowed for attribute '{Attribute.PublicAttributeName}'."); - - IsAttributeOfRelationship = _isAttributeOfRelationship; - PropertyValue = filterQuery.Value; - FilterOperation = filterQuery.OperationType; - } - - /// - /// Build AttrQuery based on SortQuery values. - /// - /// - /// - public AttrQuery(IJsonApiContext jsonApiContext, SortQuery sortQuery) - { - _jsonApiContext = jsonApiContext; - Attribute = GetAttribute(sortQuery.Attribute); - - if (Attribute.IsSortable == false) - throw new JsonApiException(400, $"Sort is not allowed for attribute '{Attribute.PublicAttributeName}'."); - - IsAttributeOfRelationship = _isAttributeOfRelationship; - Direction = sortQuery.Direction; - } - - private AttrAttribute GetAttribute(string attribute) - { - try - { - return _jsonApiContext - .RequestEntity - .Attributes - .Single(attr => attr.Is(attribute)); - } - catch (InvalidOperationException e) - { - throw new JsonApiException(400, $"Attribute '{attribute}' does not exist on resource '{_jsonApiContext.RequestEntity.EntityName}'", e); - } - } - - } -} diff --git a/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs b/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs index 4455c6df7c..0813f7649e 100644 --- a/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs @@ -6,20 +6,28 @@ namespace JsonApiDotNetCore.Internal.Query { /// - /// Abstract class to make available shared properties for AttrQuery and RelatedAttrQuery + /// 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 /// public abstract class BaseAttrQuery { - public AttrAttribute Attribute { get; protected set; } - public RelationshipAttribute RelationshipAttribute { get; protected set; } - public bool IsAttributeOfRelationship { get; protected set; } + protected BaseAttrQuery(RelationshipAttribute relationship, AttrAttribute attr) + { + Relationship = relationship; + Attr = attr; + } - // Filter properties - public string PropertyValue { get; protected set; } - public FilterOperations FilterOperation { get; protected set; } - // Sort properties - public SortDirection Direction { get; protected set; } + public AttrAttribute Attr { get; } + public RelationshipAttribute Relationship { get; } + public bool IsAttributeOfRelationship => Relationship != null; + + public string GetPropertyPath() + { + if (IsAttributeOfRelationship) + return string.Format("{0}.{1}", Relationship.InternalRelationshipName, Attr.InternalAttributeName); + else + return Attr.InternalAttributeName; + } } } diff --git a/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs new file mode 100644 index 0000000000..602b3e6a95 --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs @@ -0,0 +1,23 @@ +using JsonApiDotNetCore.Models; +using System; + +namespace JsonApiDotNetCore.Internal.Query +{ + public class BaseFilterQuery + { + protected FilterOperations GetFilterOperation(string prefix) + { + if (prefix.Length == 0) return FilterOperations.eq; + + if (Enum.TryParse(prefix, out FilterOperations opertion) == false) + throw new JsonApiException(400, $"Invalid filter prefix '{prefix}'"); + + return opertion; + } + + public AttrAttribute FilteredAttribute { get; protected set; } + public RelationshipAttribute FilteredRelationship { get; protected set; } + public string PropertyValue { get; protected set; } + public FilterOperations FilterOperation { get; protected set; } + } +} diff --git a/src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs b/src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs index 9ed1f4e99d..60ae0af012 100644 --- a/src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs +++ b/src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs @@ -1,6 +1,4 @@ // ReSharper disable InconsistentNaming -using System; - namespace JsonApiDotNetCore.Internal.Query { public enum FilterOperations @@ -17,5 +15,4 @@ public enum FilterOperations isnull = 9, isnotnull = 10 } - } diff --git a/src/JsonApiDotNetCore/Internal/Query/FilterOperationsHelper.cs b/src/JsonApiDotNetCore/Internal/Query/FilterOperationsHelper.cs deleted file mode 100644 index ba5eee3f72..0000000000 --- a/src/JsonApiDotNetCore/Internal/Query/FilterOperationsHelper.cs +++ /dev/null @@ -1,66 +0,0 @@ -// ReSharper disable InconsistentNaming -using System; -using System.Linq; - -namespace JsonApiDotNetCore.Internal.Query -{ - public class FilterOperationsHelper - { - /// - /// Get filter operation enum and value by string value. - /// Input string can contain: - /// a) property value only, then FilterOperations.eq, value is returned - /// b) filter prefix and value e.g. "prefix:value", then FilterOperations.prefix, value is returned - /// In case of prefix is provided and is not in FilterOperations enum, - /// the invalid filter prefix exception is thrown. - /// - /// - /// - public static (FilterOperations opereation,string value) GetFilterOperationAndValue(string input) - { - // value is empty - if (input.Length == 0) - return (FilterOperations.eq, input); - - // split value - var values = input.Split(QueryConstants.COLON); - // value only - if(values.Length == 1) - return (FilterOperations.eq, input); - // prefix:value - else if (values.Length == 2) - { - var (operation, succeeded) = ParseFilterOperation(values[0]); - if (succeeded == false) - throw new JsonApiException(400, $"Invalid filter prefix '{values[0]}'"); - - return (operation, values[1]); - } - // some:colon:value OR prefix:some:colon:value (datetime) - else - { - // succeeded = false means no prefix found => some value with colons(datetime) - // succeeded = true means prefix provide + some value with colons(datetime) - var (operation, succeeded) = ParseFilterOperation(values[0]); - var value = ""; - // datetime - if(succeeded == false) - value = string.Join(QueryConstants.COLON_STR, values); - else - value = string.Join(QueryConstants.COLON_STR, values.Skip(1)); - return (operation, value); - } - } - - /// - /// Returns typed operation result and info about parsing success - /// - /// String represented operation - /// - public static (FilterOperations operation, bool succeeded) ParseFilterOperation(string operation) - { - var success = Enum.TryParse(operation, out FilterOperations opertion); - return (opertion, success); - } - } -} diff --git a/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs index d71c638b6c..5a0f29f7a5 100644 --- a/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs @@ -1,27 +1,45 @@ using System; using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Models; namespace JsonApiDotNetCore.Internal.Query { - public class FilterQuery: QueryAttribute + public class FilterQuery : BaseAttrQuery { - [Obsolete("You should use constructor with strongly typed OperationType.")] + /// + /// Temporary property while constructor based on string values exists + /// + internal bool IsStringBasedInit { get; } = false; + + [Obsolete("You should use constructors with strongly typed FilterOperations and AttrAttribute or/and RelationshipAttribute parameters.")] public FilterQuery(string attribute, string value, string operation) - :base(attribute) + :base(null, null) { - Key = attribute.ToProperCase(); + Attribute = attribute; + Key = attribute.ToProperCase(); Value = value; Operation = operation; + IsStringBasedInit = true; Enum.TryParse(operation, out FilterOperations opertion); OperationType = opertion; } - public FilterQuery(string attribute, string value, FilterOperations operationType) - : base(attribute) + public FilterQuery(AttrAttribute attr, string value, FilterOperations operationType) + :base(null, attr) + { + Value = value; + OperationType = operationType; + Key = attr.PublicAttributeName.ToProperCase(); + Operation = operationType.ToString(); + } + + public FilterQuery(RelationshipAttribute relationship, AttrAttribute attr, string value, FilterOperations operationType) + :base(relationship, attr) { Value = value; OperationType = operationType; + Key = string.Format("{0}.{1}", Relationship.PublicRelationshipName, Attr.PublicAttributeName); Operation = operationType.ToString(); } @@ -30,6 +48,9 @@ public FilterQuery(string attribute, string value, FilterOperations operationTyp public string Value { get; set; } [Obsolete("Operation has been replaced by '" + nameof(OperationType) + "'. OperationType is typed enum value for Operation property. This should be default property for providing operation type, because of unsustainable string (not typed) value.")] public string Operation { get; set; } + [Obsolete("String based Attribute was replaced by '" + nameof(Attr) + "' property ('" + nameof(AttrAttribute) + "' type) ")] + public string Attribute { get; } + public FilterOperations OperationType { get; set; } } diff --git a/src/JsonApiDotNetCore/Internal/Query/QueryAttribute.cs b/src/JsonApiDotNetCore/Internal/Query/QueryAttribute.cs deleted file mode 100644 index dceba53853..0000000000 --- a/src/JsonApiDotNetCore/Internal/Query/QueryAttribute.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace JsonApiDotNetCore.Internal.Query -{ - public abstract class QueryAttribute - { - public QueryAttribute(string attribute) - { - var attributes = attribute.Split('.'); - if (attributes.Length > 1) - { - RelationshipAttribute = attributes[0]; - Attribute = attributes[1]; - IsAttributeOfRelationship = true; - } - else - { - Attribute = attribute; - IsAttributeOfRelationship = false; - } - } - - public string Attribute { get; } - public string RelationshipAttribute { get; } - public bool IsAttributeOfRelationship { get; } - } -} diff --git a/src/JsonApiDotNetCore/Internal/Query/QueryConstants.cs b/src/JsonApiDotNetCore/Internal/Query/QueryConstants.cs index 3117ff7cb3..25913ab3e6 100644 --- a/src/JsonApiDotNetCore/Internal/Query/QueryConstants.cs +++ b/src/JsonApiDotNetCore/Internal/Query/QueryConstants.cs @@ -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 = '.'; } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs new file mode 100644 index 0000000000..4956566f76 --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs @@ -0,0 +1,45 @@ +using System.Linq; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; + +namespace JsonApiDotNetCore.Internal.Query +{ + public class RelatedAttrFilterQuery : BaseFilterQuery + { + private readonly IJsonApiContext _jsonApiContext; + + public RelatedAttrFilterQuery( + IJsonApiContext jsonApiContext, + FilterQuery filterQuery) + { + _jsonApiContext = jsonApiContext; + + var relationshipArray = filterQuery.Attribute.Split(QueryConstants.DOT); + 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) + 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}'."); + + FilteredRelationship = relationship; + FilteredAttribute = attribute; + PropertyValue = filterQuery.Value; + FilterOperation = GetFilterOperation(filterQuery.Operation); + } + + private RelationshipAttribute GetRelationship(string propertyName) + => _jsonApiContext.RequestEntity.Relationships.FirstOrDefault(r => r.Is(propertyName)); + + private AttrAttribute GetAttribute(RelationshipAttribute relationship, string attribute) + { + var relatedContextExntity = _jsonApiContext.ContextGraph.GetContextEntity(relationship.Type); + return relatedContextExntity.Attributes + .FirstOrDefault(a => a.Is(attribute)); + } + } +} diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrQuery.cs deleted file mode 100644 index 57f6a05788..0000000000 --- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrQuery.cs +++ /dev/null @@ -1,92 +0,0 @@ -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; -using System; -using System.Linq; - -namespace JsonApiDotNetCore.Internal.Query -{ - public class RelatedAttrQuery: BaseAttrQuery - { - private readonly IJsonApiContext _jsonApiContext; - private readonly bool _isAttributeOfRelationship = true; - - /// - /// Build RelatedAttrQuery based on FilterQuery values. - /// - /// - /// - public RelatedAttrQuery(IJsonApiContext jsonApiContext, FilterQuery filterQuery) - { - _jsonApiContext = jsonApiContext; - - RelationshipAttribute = GetRelationshipAttribute(filterQuery.RelationshipAttribute); - Attribute = GetAttribute(RelationshipAttribute, filterQuery.Attribute); - - if (Attribute.IsFilterable == false) - throw new JsonApiException(400, $"Filter is not allowed for attribute '{Attribute.PublicAttributeName}'."); - - IsAttributeOfRelationship = _isAttributeOfRelationship; - PropertyValue = filterQuery.Value; - FilterOperation = filterQuery.OperationType; - } - - /// - /// Build RelatedAttrQuery based on SortQuery values. - /// - /// - /// - public RelatedAttrQuery(IJsonApiContext jsonApiContext, SortQuery sortQuery) - { - _jsonApiContext = jsonApiContext; - - RelationshipAttribute = GetRelationshipAttribute(sortQuery.RelationshipAttribute); - Attribute = GetAttribute(RelationshipAttribute, sortQuery.Attribute); - - if (Attribute.IsSortable == false) - throw new JsonApiException(400, $"Sort is not allowed for attribute '{Attribute.PublicAttributeName}'."); - - IsAttributeOfRelationship = _isAttributeOfRelationship; - Direction = sortQuery.Direction; - } - - /// - /// Get relationship and attribute connected by '.' character - /// - /// - /// "TodoItem.Owner" - /// - /// - public string GetRelatedPropertyPath() - { - return string.Format("{0}.{1}", RelationshipAttribute.InternalRelationshipName, Attribute.InternalAttributeName); - } - - private RelationshipAttribute GetRelationshipAttribute(string relationship) - { - try - { - return _jsonApiContext - .RequestEntity - .Relationships - .Single(attr => attr.Is(relationship)); - } - catch (InvalidOperationException e) - { - throw new JsonApiException(400, $"Relationship '{relationship}' does not exist on resource '{_jsonApiContext.RequestEntity.EntityName}'", e); - } - } - - private AttrAttribute GetAttribute(RelationshipAttribute relationship, string attribute) - { - var relatedContextExntity = _jsonApiContext.ContextGraph.GetContextEntity(relationship.Type); - try - { - return relatedContextExntity.Attributes.Single(attr => attr.Is(attribute)); - } - catch (InvalidOperationException e) - { - throw new JsonApiException(400, $"Attribute '{attribute}' does not exist on resource '{relatedContextExntity.EntityName}'", e); - } - } - } -} diff --git a/src/JsonApiDotNetCore/Internal/Query/SortQuery.cs b/src/JsonApiDotNetCore/Internal/Query/SortQuery.cs index 4bacd60431..38c2c2037e 100644 --- a/src/JsonApiDotNetCore/Internal/Query/SortQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/SortQuery.cs @@ -1,14 +1,30 @@ using JsonApiDotNetCore.Models; +using System; namespace JsonApiDotNetCore.Internal.Query { - public class SortQuery: QueryAttribute + public class SortQuery : BaseAttrQuery { - public SortQuery(SortDirection direction, string sortedAttribute) - :base(sortedAttribute) + public SortQuery(SortDirection direction, AttrAttribute sortedAttribute) + : base(null, sortedAttribute) { Direction = direction; + SortedAttribute = sortedAttribute; + if (Attr.IsSortable == false) + throw new JsonApiException(400, $"Sort is not allowed for attribute '{Attr.PublicAttributeName}'."); } + + public SortQuery(SortDirection direction, RelationshipAttribute relationship, AttrAttribute sortedAttribute) + : base(relationship, sortedAttribute) + { + Direction = direction; + SortedAttribute = sortedAttribute; + if (Attr.IsSortable == false) + throw new JsonApiException(400, $"Sort is not allowed for attribute '{Attr.PublicAttributeName}'."); + } + public SortDirection Direction { get; set; } + [Obsolete("Use generic Attr property of BaseAttrQuery instead")] + public AttrAttribute SortedAttribute { get; set; } } } diff --git a/src/JsonApiDotNetCore/Services/QueryParser.cs b/src/JsonApiDotNetCore/Services/QueryParser.cs index e671e1c2d7..d29ed848a7 100644 --- a/src/JsonApiDotNetCore/Services/QueryParser.cs +++ b/src/JsonApiDotNetCore/Services/QueryParser.cs @@ -77,6 +77,50 @@ public virtual QuerySet Parse(IQueryCollection query) return querySet; } + protected virtual List ParseFilterParameters(string key, string value) + { + // expected input = filter[id]=1 + // expected input = filter[id]=eq:1 + var queries = new List(); + var propertyName = key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1]; + + // InArray case + var arrOpVal = ParseFilterOperationAndValue(value); + if (arrOpVal.operation == FilterOperations.@in || arrOpVal.operation == FilterOperations.nin) + queries.Add(CreateFilterQuery(propertyName, arrOpVal.value, arrOpVal.operation)); + else + { + var values = value.Split(QueryConstants.COMMA); + foreach (var val in values) + { + var opVal = ParseFilterOperationAndValue(value); + var query = CreateFilterQuery(propertyName, opVal.value, opVal.operation); + queries.Add(query); + } + } + + return queries; + } + + private FilterQuery CreateFilterQuery(string propertyName, string value, FilterOperations op) + { + var properties = ParseProperties(propertyName); + AttrAttribute attr; + if (properties.Count > 1) + { + RelationshipAttribute relationshipAttr = GetRelationshipAttribute(properties[0]); + attr = GetAttribute(relationshipAttr, properties[1]); + + return new FilterQuery(relationshipAttr, attr, value, op); + } + else + { + attr = GetAttribute(properties[0]); + return new FilterQuery(attr, value, op); + } + } + + [Obsolete("Use '" + nameof(ParseFilterParameters) + "' method instead. New method provide better control over FilterQueries")] protected virtual List ParseFilterQuery(string key, string value) { // expected input = filter[id]=1 @@ -85,37 +129,95 @@ protected virtual List ParseFilterQuery(string key, string value) var propertyName = key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1]; // InArray case - var arrOpVal = FilterOperationsHelper.GetFilterOperationAndValue(value); - if (arrOpVal.opereation == FilterOperations.@in || arrOpVal.opereation == FilterOperations.nin) - queries.Add(new FilterQuery(propertyName, arrOpVal.value, arrOpVal.opereation)); + var arrOpVal = ParseFilterOperation(value); + if (arrOpVal.operation == FilterOperations.@in.ToString() || arrOpVal.operation == FilterOperations.nin.ToString()) + queries.Add(new FilterQuery(propertyName, arrOpVal.value, arrOpVal.operation)); else { var values = value.Split(QueryConstants.COMMA); foreach (var val in values) { - var opVal = FilterOperationsHelper.GetFilterOperationAndValue(val); - queries.Add(new FilterQuery(propertyName, opVal.value, opVal.opereation)); + var opVal = ParseFilterOperation(value); + queries.Add(new FilterQuery(propertyName, opVal.value, opVal.operation)); } } return queries; } - //protected virtual (string operation, string value) ParseFilterOperation(string value) - //{ - // if (value.Length < 3) - // return (string.Empty, value); + [Obsolete("Use " + nameof(ParseFilterOperationAndValue) + " method instead.")] + protected virtual (string operation, string value) ParseFilterOperation(string value) + { + if (value.Length < 3) + return (string.Empty, value); + + var operation = GetFilterOperation(value); + var values = value.Split(QueryConstants.COLON); + + if (string.IsNullOrEmpty(operation)) + return (string.Empty, value); + + value = string.Join(QueryConstants.COLON_STR, values.Skip(1)); - // var operation = FilterOperationsHelper.GetFilterOperation(value); - // var values = value.Split(QueryConstants.COLON); + return (operation, value); + } - // if (string.IsNullOrEmpty(operation)) - // return (string.Empty, value); + /// + /// Parse filter operation enum and value by string value. + /// Input string can contain: + /// a) property value only, then FilterOperations.eq, value is returned + /// b) filter prefix and value e.g. "prefix:value", then FilterOperations.prefix, value is returned + /// In case of prefix is provided and is not in FilterOperations enum, + /// the invalid filter prefix exception is thrown. + /// + /// + /// + public static (FilterOperations operation, string value) ParseFilterOperationAndValue(string input) + { + // value is empty + if (input.Length == 0) + return (FilterOperations.eq, input); + + // split value + var values = input.Split(QueryConstants.COLON); + // value only + if (values.Length == 1) + return (FilterOperations.eq, input); + // prefix:value + else if (values.Length == 2) + { + var (operation, succeeded) = ResolveFilterOperation(values[0]); + if (succeeded == false) + throw new JsonApiException(400, $"Invalid filter prefix '{values[0]}'"); - // value = string.Join(QueryConstants.COLON_STR, values.Skip(1)); + return (operation, values[1]); + } + // some:colon:value OR prefix:some:colon:value (datetime) + else + { + // succeeded = false means no prefix found => some value with colons(datetime) + // succeeded = true means prefix provide + some value with colons(datetime) + var (operation, succeeded) = ResolveFilterOperation(values[0]); + var value = ""; + // datetime + if (succeeded == false) + value = string.Join(QueryConstants.COLON_STR, values); + else + value = string.Join(QueryConstants.COLON_STR, values.Skip(1)); + return (operation, value); + } + } - // return (operation, value); - //} + /// + /// Returns typed operation result and info about parsing success + /// + /// String represented operation + /// + public static (FilterOperations operation, bool succeeded) ResolveFilterOperation(string operation) + { + var success = Enum.TryParse(operation, out FilterOperations opertion); + return (opertion, success); + } protected virtual PageQuery ParsePageQuery(PageQuery pageQuery, string key, string value) { @@ -161,12 +263,31 @@ protected virtual List ParseSortParameters(string value) propertyName = propertyName.Substring(1); } - sortParameters.Add(new SortQuery(direction, propertyName)); + var sortParam = CreateSortQuery(propertyName, direction); + sortParameters.Add(sortParam); }; return sortParameters; } + private SortQuery CreateSortQuery(string propertyName, SortDirection direction) + { + var properties = ParseProperties(propertyName); + AttrAttribute attr; + if (properties.Count > 1) + { + RelationshipAttribute relationshipAttr = GetRelationshipAttribute(properties[0]); + attr = GetAttribute(relationshipAttr, properties[1]); + + return new SortQuery(direction, relationshipAttr, attr); + } + else + { + attr = GetAttribute(properties[0]); + return new SortQuery(direction, attr); + } + } + protected virtual List ParseIncludedRelationships(string value) { return value @@ -174,6 +295,13 @@ protected virtual List ParseIncludedRelationships(string value) .ToList(); } + protected virtual List ParseProperties(string value) + { + return value + .Split(QueryConstants.DOT) + .ToList(); + } + protected virtual List ParseFieldsQuery(string key, string value) { // expected: fields[TYPE]=prop1,prop2 @@ -197,6 +325,22 @@ protected virtual List ParseFieldsQuery(string key, string value) return includedFields; } + [Obsolete("Delete also when " + nameof(ParseFilterOperation) + " deleted." )] + private string GetFilterOperation(string value) + { + var values = value.Split(QueryConstants.COLON); + + if (values.Length == 1) + return string.Empty; + + var operation = values[0]; + // remove prefix from value + if (Enum.TryParse(operation, out FilterOperations op) == false) + return string.Empty; + + return operation; + } + protected virtual AttrAttribute GetAttribute(string attribute) { try @@ -212,5 +356,33 @@ protected virtual AttrAttribute GetAttribute(string attribute) } } + protected virtual AttrAttribute GetAttribute(RelationshipAttribute relationship, string attribute) + { + var relatedContextExntity = _options.ContextGraph.GetContextEntity(relationship.Type); + try + { + return relatedContextExntity.Attributes.Single(attr => attr.Is(attribute)); + } + catch (InvalidOperationException e) + { + throw new JsonApiException(400, $"Attribute '{attribute}' does not exist on resource '{relatedContextExntity.EntityName}'", e); + } + } + + protected virtual RelationshipAttribute GetRelationshipAttribute(string relationshipAttribute) + { + try + { + return _controllerContext + .RequestEntity + .Relationships + .Single(attr => attr.Is(relationshipAttribute)); + } + catch (InvalidOperationException e) + { + throw new JsonApiException(400, $"Relationship '{relationshipAttribute}' does not exist on resource '{_controllerContext.RequestEntity.EntityName}'", e); + } + } + } } diff --git a/test/UnitTests/Services/QueryParser_Tests.cs b/test/UnitTests/Services/QueryParser_Tests.cs index be6b8c5020..cfc92c2b66 100644 --- a/test/UnitTests/Services/QueryParser_Tests.cs +++ b/test/UnitTests/Services/QueryParser_Tests.cs @@ -3,6 +3,7 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Query; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Http; @@ -99,7 +100,8 @@ public void Filters_Properly_Parses_DateTime_Without_Operation() // assert Assert.Equal(dt, querySet.Filters.Single(f => f.Attribute == "key").Value); - Assert.Equal("eq", querySet.Filters.Single(f => f.Attribute == "key").Operation); + Assert.Equal(string.Empty, querySet.Filters.Single(f => f.Attribute == "key").Operation); + Assert.Equal(FilterOperations.eq, querySet.Filters.Single(f => f.Attribute == "key").OperationType); } [Fact] From 61daceac2d71d1e313a3a1d46de4765e198a384c Mon Sep 17 00:00:00 2001 From: Milos Date: Sun, 30 Sep 2018 21:13:09 +0200 Subject: [PATCH 08/18] Nested sort - feature with refactor --- .../Data/DefaultEntityRepository.cs | 2 +- .../Extensions/IQueryableExtensions.cs | 287 +++++++++++------- .../Internal/Query/AttrFilterQuery.cs | 27 +- .../Internal/Query/AttrSortQuery.cs | 26 ++ .../Internal/Query/BaseAttrQuery.cs | 35 ++- .../Internal/Query/BaseFilterQuery.cs | 22 +- .../Internal/Query/BaseQuery.cs | 26 ++ .../Internal/Query/FilterQuery.cs | 31 +- .../Internal/Query/RelatedAttrFilterQuery.cs | 37 +-- .../Internal/Query/RelatedAttrSortQuery.cs | 29 ++ .../Internal/Query/SortQuery.cs | 18 +- .../Services/QueryComposer.cs | 3 +- src/JsonApiDotNetCore/Services/QueryParser.cs | 124 +------- .../Acceptance/Spec/SparseFieldSetTests.cs | 3 +- test/UnitTests/Services/QueryComposerTests.cs | 2 +- test/UnitTests/Services/QueryParser_Tests.cs | 1 - 16 files changed, 356 insertions(+), 317 deletions(-) create mode 100644 src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs create mode 100644 src/JsonApiDotNetCore/Internal/Query/BaseQuery.cs create mode 100644 src/JsonApiDotNetCore/Internal/Query/RelatedAttrSortQuery.cs diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 27fda5629b..3021bd9390 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -72,7 +72,7 @@ public virtual IQueryable Filter(IQueryable entities, FilterQu /// public virtual IQueryable Sort(IQueryable entities, List sortQueries) { - return entities.Sort(sortQueries); + return entities.Sort(_jsonApiContext,sortQueries); } /// diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index 51e0865856..8fd6e2b9d1 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -29,6 +29,7 @@ private static MethodInfo ContainsMethod } } + [Obsolete("Use Sort method with IJsonApiContext parameter instead. New Sort method provides nested sorting.")] public static IQueryable Sort(this IQueryable source, List sortQueries) { if (sortQueries == null || sortQueries.Count == 0) @@ -45,21 +46,83 @@ public static IQueryable Sort(this IQueryable source, return orderedEntities; } + [Obsolete("Use Sort method with IJsonApiContext parameter instead. New Sort method provides nested sorting.")] public static IOrderedQueryable Sort(this IQueryable source, SortQuery sortQuery) { - var path = sortQuery.GetPropertyPath(); + // For clients using SortQuery constructor with string based parameter + if (sortQuery.SortedAttribute == null) + throw new JsonApiException(400, $"It's not possible to provide {nameof(SortQuery)} without {nameof(SortQuery.SortedAttribute)} parameter." + + $" Use Sort method with IJsonApiContext parameter instead."); + return sortQuery.Direction == SortDirection.Descending - ? source.OrderByDescending(path) - : source.OrderBy(path); + ? source.OrderByDescending(sortQuery.SortedAttribute.InternalAttributeName) + : source.OrderBy(sortQuery.SortedAttribute.InternalAttributeName); } + [Obsolete("Use Sort method with IJsonApiContext parameter instead. New Sort method provides nested sorting.")] public static IOrderedQueryable Sort(this IOrderedQueryable source, SortQuery sortQuery) { - var path = sortQuery.GetPropertyPath(); + // For clients using SortQuery constructor with string based parameter + if (sortQuery.SortedAttribute == null) + throw new JsonApiException(400, $"It's not possible to provide {nameof(SortQuery)} without {nameof(SortQuery.SortedAttribute)} parameter." + + $" Use Sort method with IJsonApiContext parameter instead."); + + return sortQuery.Direction == SortDirection.Descending + ? source.OrderByDescending(sortQuery.SortedAttribute.InternalAttributeName) + : source.OrderBy(sortQuery.SortedAttribute.InternalAttributeName); + } + + public static IQueryable Sort(this IQueryable source, IJsonApiContext jsonApiContext, List sortQueries) + { + if (sortQueries == null || sortQueries.Count == 0) + return source; + + var orderedEntities = source.Sort(jsonApiContext, sortQueries[0]); + + if (sortQueries.Count <= 1) + return orderedEntities; + + for (var i = 1; i < sortQueries.Count; i++) + orderedEntities = orderedEntities.Sort(jsonApiContext, sortQueries[i]); + + return orderedEntities; + } + + public static IOrderedQueryable Sort(this IQueryable source, IJsonApiContext jsonApiContext, SortQuery sortQuery) + { + // For clients using constructor with AttrAttribute parameter + if (sortQuery.SortedAttribute != null) + throw new JsonApiException(400, $"It's not possible to provide {nameof(SortQuery)} with {nameof(SortQuery.SortedAttribute)} parameter." + + $" Use {nameof(SortQuery)} constructor overload based on string attribute."); + + BaseAttrQuery attr; + if (sortQuery.IsAttributeOfRelationship) + attr = new RelatedAttrSortQuery(jsonApiContext, sortQuery); + else + attr = new AttrSortQuery(jsonApiContext, sortQuery); + return sortQuery.Direction == SortDirection.Descending - ? source.OrderByDescending(path) - : source.OrderBy(path); - } + ? source.OrderByDescending(attr.GetPropertyPath()) + : source.OrderBy(attr.GetPropertyPath()); + } + + public static IOrderedQueryable Sort(this IOrderedQueryable source, IJsonApiContext jsonApiContext, SortQuery sortQuery) + { + // For clients using constructor with AttrAttribute parameter + if (sortQuery.SortedAttribute != null) + throw new JsonApiException(400, $"It's not possible to provide {nameof(SortQuery)} with {nameof(SortQuery.SortedAttribute)} parameter." + + $" Use {nameof(SortQuery)} constructor overload based on string attribute."); + + BaseAttrQuery attr; + if (sortQuery.IsAttributeOfRelationship) + attr = new RelatedAttrSortQuery(jsonApiContext, sortQuery); + else + attr = new AttrSortQuery(jsonApiContext, sortQuery); + + return sortQuery.Direction == SortDirection.Descending + ? source.OrderByDescending(attr.GetPropertyPath()) + : source.OrderBy(attr.GetPropertyPath()); + } public static IOrderedQueryable OrderBy(this IQueryable source, string propertyName) => CallGenericOrderMethod(source, propertyName, "OrderBy"); @@ -73,14 +136,42 @@ public static IOrderedQueryable ThenBy(this IOrderedQueryable< public static IOrderedQueryable ThenByDescending(this IOrderedQueryable source, string propertyName) => CallGenericOrderMethod(source, propertyName, "ThenByDescending"); + private static IOrderedQueryable CallGenericOrderMethod(IQueryable source, string propertyName, string method) + { + // {x} + var parameter = Expression.Parameter(typeof(TSource), "x"); + MemberExpression member; + + var values = propertyName.Split('.'); + if (values.Length > 1) + { + var relation = Expression.PropertyOrField(parameter, values[0]); + // {x.relationship.propertyName} + member = Expression.Property(relation, values[1]); + } + else + { + // {x.propertyName} + member = Expression.Property(parameter, values[0]); + } + // {x=>x.propertyName} or {x=>x.relationship.propertyName} + var lambda = Expression.Lambda(member, parameter); + + // REFLECTION: source.OrderBy(x => x.Property) + var orderByMethod = typeof(Queryable).GetMethods().First(x => x.Name == method && x.GetParameters().Length == 2); + var orderByGeneric = orderByMethod.MakeGenericMethod(typeof(TSource), member.Type); + var result = orderByGeneric.Invoke(null, new object[] { source, lambda }); + + return (IOrderedQueryable)result; + } + public static IQueryable Filter(this IQueryable source, IJsonApiContext jsonApiContext, FilterQuery filterQuery) { if (filterQuery == null) return source; // Relationship.Attribute - if ((filterQuery.IsStringBasedInit && filterQuery.Attribute.Contains(QueryConstants.DOT)) - || filterQuery.IsAttributeOfRelationship) + if (filterQuery.IsAttributeOfRelationship) return source.Filter(new RelatedAttrFilterQuery(jsonApiContext, filterQuery)); return source.Filter(new AttrFilterQuery(jsonApiContext, filterQuery)); @@ -144,77 +235,48 @@ private static Expression GetFilterExpressionLambda(Expression left, Expression return body; } - public static IQueryable Select(this IQueryable source, List columns) + private static IQueryable CallGenericWhereContainsMethod(IQueryable source, BaseFilterQuery filter) { - 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( - Expression.Call(typeof(Queryable), "Select", new[] { sourceType, resultType }, - source.Expression, Expression.Quote(selector))); - } + var concreteType = typeof(TSource); + var property = concreteType.GetProperty(filter.Attribute.InternalAttributeName); - public static IQueryable PageForward(this IQueryable source, int pageSize, int pageNumber) - { - if (pageSize > 0) + try { - if (pageNumber == 0) - pageNumber = 1; - - if (pageNumber > 0) - return source - .Skip((pageNumber - 1) * pageSize) - .Take(pageSize); - } + var propertyValues = filter.PropertyValue.Split(QueryConstants.COMMA); + ParameterExpression entity = Expression.Parameter(concreteType, "entity"); + MemberExpression member; + if (filter.IsAttributeOfRelationship) + { + var relation = Expression.PropertyOrField(entity, filter.Relationship.InternalRelationshipName); + member = Expression.Property(relation, filter.Attribute.InternalAttributeName); + } + else + member = Expression.Property(entity, filter.Attribute.InternalAttributeName); - return source; - } + var method = ContainsMethod.MakeGenericMethod(member.Type); + var obj = TypeHelper.ConvertListType(propertyValues, member.Type); - #region Generic method calls + if (filter.FilterOperation == FilterOperations.@in) + { + // Where(i => arr.Contains(i.column)) + var contains = Expression.Call(method, new Expression[] { Expression.Constant(obj), member }); + var lambda = Expression.Lambda>(contains, entity); - private static IOrderedQueryable CallGenericOrderMethod(IQueryable source, string propertyName, string method) - { - // {x} - var parameter = Expression.Parameter(typeof(TSource), "x"); - MemberExpression member; + return source.Where(lambda); + } + else + { + // Where(i => !arr.Contains(i.column)) + var notContains = Expression.Not(Expression.Call(method, new Expression[] { Expression.Constant(obj), member })); + var lambda = Expression.Lambda>(notContains, entity); - var values = propertyName.Split('.'); - if (values.Length > 1) - { - var relation = Expression.PropertyOrField(parameter, values[0]); - // {x.relationship.propertyName} - member = Expression.Property(relation, values[1]); + return source.Where(lambda); + } } - else + catch (FormatException) { - // {x.propertyName} - member = Expression.Property(parameter, values[0]); + throw new JsonApiException(400, $"Could not cast {filter.PropertyValue} to {property.PropertyType.Name}"); } - // {x=>x.propertyName} or {x=>x.relationship.propertyName} - var lambda = Expression.Lambda(member, parameter); - - // REFLECTION: source.OrderBy(x => x.Property) - var orderByMethod = typeof(Queryable).GetMethods().First(x => x.Name == method && x.GetParameters().Length == 2); - var orderByGeneric = orderByMethod.MakeGenericMethod(typeof(TSource), member.Type); - var result = orderByGeneric.Invoke(null, new object[] { source, lambda }); - - return (IOrderedQueryable)result; } private static IQueryable CallGenericWhereMethod(IQueryable source, BaseFilterQuery filter) @@ -229,27 +291,27 @@ private static IQueryable CallGenericWhereMethod(IQueryable CallGenericWhereMethod(IQueryable CallGenericWhereContainsMethod(IQueryable source, BaseFilterQuery filter) + public static IQueryable Select(this IQueryable source, List columns) { - var concreteType = typeof(TSource); - var property = concreteType.GetProperty(filter.FilteredAttribute.InternalAttributeName); + if (columns == null || columns.Count == 0) + return source; - try - { - var propertyValues = filter.PropertyValue.Split(QueryConstants.COMMA); - ParameterExpression entity = Expression.Parameter(concreteType, "entity"); - MemberExpression member; - if (filter.FilteredRelationship != null) - { - var relation = Expression.PropertyOrField(entity, filter.FilteredRelationship.InternalRelationshipName); - member = Expression.Property(relation, filter.FilteredAttribute.InternalAttributeName); - } - else - member = Expression.Property(entity, filter.FilteredAttribute.InternalAttributeName); + var sourceType = source.ElementType; - var method = ContainsMethod.MakeGenericMethod(member.Type); - var obj = TypeHelper.ConvertListType(propertyValues, member.Type); + var resultType = typeof(TSource); - if (filter.FilterOperation == FilterOperations.@in) - { - // Where(i => arr.Contains(i.column)) - var contains = Expression.Call(method, new Expression[] { Expression.Constant(obj), member }); - var lambda = Expression.Lambda>(contains, entity); + // {model} + var parameter = Expression.Parameter(sourceType, "model"); - return source.Where(lambda); - } - else - { - // Where(i => !arr.Contains(i.column)) - var notContains = Expression.Not(Expression.Call(method, new Expression[] { Expression.Constant(obj), member })); - var lambda = Expression.Lambda>(notContains, entity); + var bindings = columns.Select(column => Expression.Bind( + resultType.GetProperty(column), Expression.PropertyOrField(parameter, column))); - return source.Where(lambda); - } - } - catch (FormatException) + // { 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( + Expression.Call(typeof(Queryable), "Select", new[] { sourceType, resultType }, + source.Expression, Expression.Quote(selector))); + } + + public static IQueryable PageForward(this IQueryable source, int pageSize, int pageNumber) + { + if (pageSize > 0) { - throw new JsonApiException(400, $"Could not cast {filter.PropertyValue} to {property.PropertyType.Name}"); + if (pageNumber == 0) + pageNumber = 1; + + if (pageNumber > 0) + return source + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize); } + + return source; } - #endregion } } diff --git a/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs index 6571e5b9e6..575472af06 100644 --- a/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; @@ -6,29 +7,25 @@ namespace JsonApiDotNetCore.Internal.Query { public class AttrFilterQuery : BaseFilterQuery { - private readonly IJsonApiContext _jsonApiContext; - public AttrFilterQuery( IJsonApiContext jsonApiContext, FilterQuery filterQuery) + : base(jsonApiContext, + null, + filterQuery.Attribute, + filterQuery.Value, + filterQuery.OperationType) { - _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; } - - private AttrAttribute GetAttribute(string attribute) => - _jsonApiContext.RequestEntity.Attributes.FirstOrDefault(attr => attr.Is(attribute)); + [Obsolete("Use " + nameof(Attribute) + " property of " + nameof(BaseAttrQuery) + "class. This property is shared for all AttrQuery and RelatedAttrQuery (filter,sort..) implementations.")] + public AttrAttribute FilteredAttribute { get; set; } } } diff --git a/src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs b/src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs new file mode 100644 index 0000000000..9243f05eef --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs @@ -0,0 +1,26 @@ +using System; +using System.Linq; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; + +namespace JsonApiDotNetCore.Internal.Query +{ + public class AttrSortQuery : BaseAttrQuery + { + public AttrSortQuery( + IJsonApiContext jsonApiContext, + SortQuery sortQuery) + :base(jsonApiContext, null, sortQuery.Attribute) + { + 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; } + } +} diff --git a/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs b/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs index 0813f7649e..3ffff039e1 100644 --- a/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs @@ -1,6 +1,5 @@ using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; -using System; using System.Linq; namespace JsonApiDotNetCore.Internal.Query @@ -12,22 +11,44 @@ namespace JsonApiDotNetCore.Internal.Query /// public abstract class BaseAttrQuery { - protected BaseAttrQuery(RelationshipAttribute relationship, AttrAttribute attr) + private readonly IJsonApiContext _jsonApiContext; + + public BaseAttrQuery(IJsonApiContext jsonApiContext, string relationship, string attribute) { - Relationship = relationship; - Attr = attr; + _jsonApiContext = jsonApiContext; + if (string.IsNullOrEmpty(relationship)) + Attribute = GetAttribute(attribute); + else + { + Relationship = GetRelationship(relationship); + Attribute = GetAttribute(Relationship, attribute); + } + } - public AttrAttribute Attr { get; } + 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, Attr.InternalAttributeName); + return string.Format("{0}.{1}", Relationship.InternalRelationshipName, Attribute.InternalAttributeName); else - return Attr.InternalAttributeName; + 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.ContextGraph.GetContextEntity(relationship.Type); + return relatedContextExntity.Attributes + .FirstOrDefault(a => a.Is(attribute)); } } } diff --git a/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs index 602b3e6a95..49da22ed84 100644 --- a/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs @@ -1,10 +1,24 @@ using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; using System; namespace JsonApiDotNetCore.Internal.Query { - public class BaseFilterQuery + public abstract class BaseFilterQuery : BaseAttrQuery { + public BaseFilterQuery( + IJsonApiContext jsonApiContext, + string relationship, + string attribute, + string value, + FilterOperations op) + : base(jsonApiContext, relationship, attribute) + { + PropertyValue = value; + FilterOperation = op; + } + + [Obsolete("To resolve operation use enum typed " + nameof(FilterQuery.OperationType) + " property of "+ nameof(FilterQuery) +" class")] protected FilterOperations GetFilterOperation(string prefix) { if (prefix.Length == 0) return FilterOperations.eq; @@ -15,9 +29,7 @@ protected FilterOperations GetFilterOperation(string prefix) return opertion; } - public AttrAttribute FilteredAttribute { get; protected set; } - public RelationshipAttribute FilteredRelationship { get; protected set; } - public string PropertyValue { get; protected set; } - public FilterOperations FilterOperation { get; protected set; } + public string PropertyValue { get; } + public FilterOperations FilterOperation { get; } } } diff --git a/src/JsonApiDotNetCore/Internal/Query/BaseQuery.cs b/src/JsonApiDotNetCore/Internal/Query/BaseQuery.cs new file mode 100644 index 0000000000..90830196c4 --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Query/BaseQuery.cs @@ -0,0 +1,26 @@ +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; +using System; +using System.Linq; + +namespace JsonApiDotNetCore.Internal.Query +{ + public abstract class BaseQuery + { + 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; + } +} diff --git a/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs index 5a0f29f7a5..178ca5ca5a 100644 --- a/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs @@ -4,43 +4,28 @@ namespace JsonApiDotNetCore.Internal.Query { - public class FilterQuery : BaseAttrQuery + public class FilterQuery : BaseQuery { - /// - /// Temporary property while constructor based on string values exists - /// - internal bool IsStringBasedInit { get; } = false; - - [Obsolete("You should use constructors with strongly typed FilterOperations and AttrAttribute or/and RelationshipAttribute parameters.")] + [Obsolete("Use constructor with FilterOperations operationType paremeter. Filter operation should be provided " + + "as enum type, not by string.")] public FilterQuery(string attribute, string value, string operation) - :base(null, null) + :base(attribute) { - Attribute = attribute; Key = attribute.ToProperCase(); Value = value; Operation = operation; - IsStringBasedInit = true; Enum.TryParse(operation, out FilterOperations opertion); OperationType = opertion; } - public FilterQuery(AttrAttribute attr, string value, FilterOperations operationType) - :base(null, attr) + public FilterQuery(string attribute, string value, FilterOperations operationType) + : base(attribute) { + Key = attribute.ToProperCase(); Value = value; - OperationType = operationType; - Key = attr.PublicAttributeName.ToProperCase(); Operation = operationType.ToString(); - } - - public FilterQuery(RelationshipAttribute relationship, AttrAttribute attr, string value, FilterOperations operationType) - :base(relationship, attr) - { - Value = value; OperationType = operationType; - Key = string.Format("{0}.{1}", Relationship.PublicRelationshipName, Attr.PublicAttributeName); - Operation = operationType.ToString(); } [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.")] @@ -48,8 +33,6 @@ public FilterQuery(RelationshipAttribute relationship, AttrAttribute attr, strin public string Value { get; set; } [Obsolete("Operation has been replaced by '" + nameof(OperationType) + "'. OperationType is typed enum value for Operation property. This should be default property for providing operation type, because of unsustainable string (not typed) value.")] public string Operation { get; set; } - [Obsolete("String based Attribute was replaced by '" + nameof(Attr) + "' property ('" + nameof(AttrAttribute) + "' type) ")] - public string Attribute { get; } public FilterOperations OperationType { get; set; } diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs index 4956566f76..56d13ce98c 100644 --- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; @@ -6,40 +7,28 @@ namespace JsonApiDotNetCore.Internal.Query { public class RelatedAttrFilterQuery : BaseFilterQuery { - private readonly IJsonApiContext _jsonApiContext; - public RelatedAttrFilterQuery( IJsonApiContext jsonApiContext, FilterQuery filterQuery) + :base(jsonApiContext, filterQuery.Relationship, filterQuery.Attribute, filterQuery.Value, filterQuery.OperationType) { - _jsonApiContext = jsonApiContext; - - var relationshipArray = filterQuery.Attribute.Split(QueryConstants.DOT); - var relationship = GetRelationship(relationshipArray[0]); - if (relationship == null) - throw new JsonApiException(400, $"{relationshipArray[1]} is not a valid relationship on {relationshipArray[0]}."); + if (Relationship == null) + throw new JsonApiException(400, $"{filterQuery.Relationship} is not a valid relationship on {jsonApiContext.RequestEntity.EntityName}."); - 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; } - private RelationshipAttribute GetRelationship(string propertyName) - => _jsonApiContext.RequestEntity.Relationships.FirstOrDefault(r => r.Is(propertyName)); + [Obsolete("Use " + nameof(Attribute) + " property. It's shared for all implementations of BaseAttrQuery(better sort, filter) handling")] + public AttrAttribute FilteredAttribute { get; set; } - private AttrAttribute GetAttribute(RelationshipAttribute relationship, string attribute) - { - var relatedContextExntity = _jsonApiContext.ContextGraph.GetContextEntity(relationship.Type); - return relatedContextExntity.Attributes - .FirstOrDefault(a => a.Is(attribute)); - } + [Obsolete("Use " + nameof(Relationship) + " property. It's shared for all implementations of BaseAttrQuery(better sort, filter) handling")] + public RelationshipAttribute FilteredRelationship { get; set; } } } diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrSortQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrSortQuery.cs new file mode 100644 index 0000000000..aea411f569 --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrSortQuery.cs @@ -0,0 +1,29 @@ +using System; +using System.Linq; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; + +namespace JsonApiDotNetCore.Internal.Query +{ + public class RelatedAttrSortQuery : BaseAttrQuery + { + public RelatedAttrSortQuery( + IJsonApiContext jsonApiContext, + SortQuery sortQuery) + :base(jsonApiContext, sortQuery.Relationship, sortQuery.Attribute) + { + if (Relationship == null) + throw new JsonApiException(400, $"{sortQuery.Relationship} is not a valid relationship on {jsonApiContext.RequestEntity.EntityName}."); + + 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; } + } +} diff --git a/src/JsonApiDotNetCore/Internal/Query/SortQuery.cs b/src/JsonApiDotNetCore/Internal/Query/SortQuery.cs index 38c2c2037e..728c770e84 100644 --- a/src/JsonApiDotNetCore/Internal/Query/SortQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/SortQuery.cs @@ -3,28 +3,26 @@ namespace JsonApiDotNetCore.Internal.Query { - public class SortQuery : BaseAttrQuery + public class SortQuery : BaseQuery { + [Obsolete("Use constructor with string attribute parameter. New constructor provides nested sort feature.")] public SortQuery(SortDirection direction, AttrAttribute sortedAttribute) - : base(null, sortedAttribute) + :base(sortedAttribute.InternalAttributeName) { Direction = direction; SortedAttribute = sortedAttribute; - if (Attr.IsSortable == false) - throw new JsonApiException(400, $"Sort is not allowed for attribute '{Attr.PublicAttributeName}'."); + if (SortedAttribute.IsSortable == false) + throw new JsonApiException(400, $"Sort is not allowed for attribute '{SortedAttribute.PublicAttributeName}'."); } - public SortQuery(SortDirection direction, RelationshipAttribute relationship, AttrAttribute sortedAttribute) - : base(relationship, sortedAttribute) + public SortQuery(SortDirection direction, string attribute) + : base(attribute) { Direction = direction; - SortedAttribute = sortedAttribute; - if (Attr.IsSortable == false) - throw new JsonApiException(400, $"Sort is not allowed for attribute '{Attr.PublicAttributeName}'."); } public SortDirection Direction { get; set; } - [Obsolete("Use generic Attr property of BaseAttrQuery instead")] + [Obsolete("Use string based Attribute instead. This provides nested sort feature (e.g. ?sort=owner.first-name)")] public AttrAttribute SortedAttribute { get; set; } } } diff --git a/src/JsonApiDotNetCore/Services/QueryComposer.cs b/src/JsonApiDotNetCore/Services/QueryComposer.cs index e365811704..28d7c927fb 100644 --- a/src/JsonApiDotNetCore/Services/QueryComposer.cs +++ b/src/JsonApiDotNetCore/Services/QueryComposer.cs @@ -30,8 +30,7 @@ public string Compose(IJsonApiContext jsonApiContext) private string ComposeSingleFilter(FilterQuery query) { var result = "&filter"; - var operation = string.IsNullOrWhiteSpace(query.Operation) ? query.Operation : query.Operation + ":"; - result += QueryConstants.OPEN_BRACKET + query.Attribute + QueryConstants.CLOSE_BRACKET + "=" + operation + query.Value; + result += QueryConstants.OPEN_BRACKET + query.Attribute + QueryConstants.CLOSE_BRACKET + "=" + query.OperationType + QueryConstants.COLON + query.Value; return result; } } diff --git a/src/JsonApiDotNetCore/Services/QueryParser.cs b/src/JsonApiDotNetCore/Services/QueryParser.cs index d29ed848a7..f4f5982cc5 100644 --- a/src/JsonApiDotNetCore/Services/QueryParser.cs +++ b/src/JsonApiDotNetCore/Services/QueryParser.cs @@ -77,7 +77,7 @@ public virtual QuerySet Parse(IQueryCollection query) return querySet; } - protected virtual List ParseFilterParameters(string key, string value) + protected virtual List ParseFilterQuery(string key, string value) { // expected input = filter[id]=1 // expected input = filter[id]=eq:1 @@ -87,57 +87,13 @@ protected virtual List ParseFilterParameters(string key, string val // InArray case var arrOpVal = ParseFilterOperationAndValue(value); if (arrOpVal.operation == FilterOperations.@in || arrOpVal.operation == FilterOperations.nin) - queries.Add(CreateFilterQuery(propertyName, arrOpVal.value, arrOpVal.operation)); - else - { - var values = value.Split(QueryConstants.COMMA); - foreach (var val in values) - { - var opVal = ParseFilterOperationAndValue(value); - var query = CreateFilterQuery(propertyName, opVal.value, opVal.operation); - queries.Add(query); - } - } - - return queries; - } - - private FilterQuery CreateFilterQuery(string propertyName, string value, FilterOperations op) - { - var properties = ParseProperties(propertyName); - AttrAttribute attr; - if (properties.Count > 1) - { - RelationshipAttribute relationshipAttr = GetRelationshipAttribute(properties[0]); - attr = GetAttribute(relationshipAttr, properties[1]); - - return new FilterQuery(relationshipAttr, attr, value, op); - } - else - { - attr = GetAttribute(properties[0]); - return new FilterQuery(attr, value, op); - } - } - - [Obsolete("Use '" + nameof(ParseFilterParameters) + "' method instead. New method provide better control over FilterQueries")] - protected virtual List ParseFilterQuery(string key, string value) - { - // expected input = filter[id]=1 - // expected input = filter[id]=eq:1 - var queries = new List(); - var propertyName = key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1]; - - // InArray case - var arrOpVal = ParseFilterOperation(value); - if (arrOpVal.operation == FilterOperations.@in.ToString() || arrOpVal.operation == FilterOperations.nin.ToString()) queries.Add(new FilterQuery(propertyName, arrOpVal.value, arrOpVal.operation)); else { var values = value.Split(QueryConstants.COMMA); foreach (var val in values) { - var opVal = ParseFilterOperation(value); + var opVal = ParseFilterOperationAndValue(value); queries.Add(new FilterQuery(propertyName, opVal.value, opVal.operation)); } } @@ -145,13 +101,14 @@ protected virtual List ParseFilterQuery(string key, string value) return queries; } - [Obsolete("Use " + nameof(ParseFilterOperationAndValue) + " method instead.")] + [Obsolete("This method is not used anymore! Use " + nameof(ParseFilterOperationAndValue) + " method with FilterOperations operation return value." + + "Operation as string is not used at all.")] protected virtual (string operation, string value) ParseFilterOperation(string value) { if (value.Length < 3) return (string.Empty, value); - var operation = GetFilterOperation(value); + var operation = GetFilterOperationOld(value); var values = value.Split(QueryConstants.COLON); if (string.IsNullOrEmpty(operation)) @@ -165,10 +122,10 @@ protected virtual (string operation, string value) ParseFilterOperation(string v /// /// Parse filter operation enum and value by string value. /// Input string can contain: - /// a) property value only, then FilterOperations.eq, value is returned - /// b) filter prefix and value e.g. "prefix:value", then FilterOperations.prefix, value is returned + /// a) property value only, then FilterOperations.eq and value is returned + /// b) filter prefix and value e.g. "prefix:value", then FilterOperations.prefix and value is returned /// In case of prefix is provided and is not in FilterOperations enum, - /// the invalid filter prefix exception is thrown. + /// invalid filter prefix exception is thrown. /// /// /// @@ -186,7 +143,7 @@ public static (FilterOperations operation, string value) ParseFilterOperationAnd // prefix:value else if (values.Length == 2) { - var (operation, succeeded) = ResolveFilterOperation(values[0]); + var (operation, succeeded) = GetFilterOperation(values[0]); if (succeeded == false) throw new JsonApiException(400, $"Invalid filter prefix '{values[0]}'"); @@ -197,7 +154,7 @@ public static (FilterOperations operation, string value) ParseFilterOperationAnd { // succeeded = false means no prefix found => some value with colons(datetime) // succeeded = true means prefix provide + some value with colons(datetime) - var (operation, succeeded) = ResolveFilterOperation(values[0]); + var (operation, succeeded) = GetFilterOperation(values[0]); var value = ""; // datetime if (succeeded == false) @@ -213,7 +170,7 @@ public static (FilterOperations operation, string value) ParseFilterOperationAnd /// /// String represented operation /// - public static (FilterOperations operation, bool succeeded) ResolveFilterOperation(string operation) + public static (FilterOperations operation, bool succeeded) GetFilterOperation(string operation) { var success = Enum.TryParse(operation, out FilterOperations opertion); return (opertion, success); @@ -263,31 +220,12 @@ protected virtual List ParseSortParameters(string value) propertyName = propertyName.Substring(1); } - var sortParam = CreateSortQuery(propertyName, direction); - sortParameters.Add(sortParam); + sortParameters.Add(new SortQuery(direction, propertyName)); }; return sortParameters; } - private SortQuery CreateSortQuery(string propertyName, SortDirection direction) - { - var properties = ParseProperties(propertyName); - AttrAttribute attr; - if (properties.Count > 1) - { - RelationshipAttribute relationshipAttr = GetRelationshipAttribute(properties[0]); - attr = GetAttribute(relationshipAttr, properties[1]); - - return new SortQuery(direction, relationshipAttr, attr); - } - else - { - attr = GetAttribute(properties[0]); - return new SortQuery(direction, attr); - } - } - protected virtual List ParseIncludedRelationships(string value) { return value @@ -295,13 +233,6 @@ protected virtual List ParseIncludedRelationships(string value) .ToList(); } - protected virtual List ParseProperties(string value) - { - return value - .Split(QueryConstants.DOT) - .ToList(); - } - protected virtual List ParseFieldsQuery(string key, string value) { // expected: fields[TYPE]=prop1,prop2 @@ -326,7 +257,7 @@ protected virtual List ParseFieldsQuery(string key, string value) } [Obsolete("Delete also when " + nameof(ParseFilterOperation) + " deleted." )] - private string GetFilterOperation(string value) + private string GetFilterOperationOld(string value) { var values = value.Split(QueryConstants.COLON); @@ -355,34 +286,5 @@ protected virtual AttrAttribute GetAttribute(string attribute) throw new JsonApiException(400, $"Attribute '{attribute}' does not exist on resource '{_controllerContext.RequestEntity.EntityName}'", e); } } - - protected virtual AttrAttribute GetAttribute(RelationshipAttribute relationship, string attribute) - { - var relatedContextExntity = _options.ContextGraph.GetContextEntity(relationship.Type); - try - { - return relatedContextExntity.Attributes.Single(attr => attr.Is(attribute)); - } - catch (InvalidOperationException e) - { - throw new JsonApiException(400, $"Attribute '{attribute}' does not exist on resource '{relatedContextExntity.EntityName}'", e); - } - } - - protected virtual RelationshipAttribute GetRelationshipAttribute(string relationshipAttribute) - { - try - { - return _controllerContext - .RequestEntity - .Relationships - .Single(attr => attr.Is(relationshipAttribute)); - } - catch (InvalidOperationException e) - { - throw new JsonApiException(400, $"Relationship '{relationshipAttribute}' does not exist on resource '{_controllerContext.RequestEntity.EntityName}'", e); - } - } - } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs index cb65191ef1..a07fe79201 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs @@ -4,7 +4,6 @@ using System.Net.Http; using System.Threading.Tasks; using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Internal.Query; using JsonApiDotNetCore.Models; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; @@ -35,7 +34,7 @@ public SparseFieldSetTests(TestFixture fixture) public async Task Can_Select_Sparse_Fieldsets() { // arrange - var fields = new List { "Id","Description", "CreatedDate", "AchievedDate" }; + var fields = new List { "Id", "Description", "CreatedDate", "AchievedDate" }; var todoItem = new TodoItem { Description = "description", Ordinal = 1, diff --git a/test/UnitTests/Services/QueryComposerTests.cs b/test/UnitTests/Services/QueryComposerTests.cs index 330083820c..0a341c1c99 100644 --- a/test/UnitTests/Services/QueryComposerTests.cs +++ b/test/UnitTests/Services/QueryComposerTests.cs @@ -56,7 +56,7 @@ public void Can_ComposeLessThan_FilterStringForUrl() // act var filterString = queryComposer.Compose(_jsonApiContext.Object); // assert - Assert.Equal("&filter[attribute]=le:value&filter[attribute2]=value2", filterString); + Assert.Equal("&filter[attribute]=le:value&filter[attribute2]=eq:value2", filterString); } [Fact] diff --git a/test/UnitTests/Services/QueryParser_Tests.cs b/test/UnitTests/Services/QueryParser_Tests.cs index cfc92c2b66..5cd2de16ce 100644 --- a/test/UnitTests/Services/QueryParser_Tests.cs +++ b/test/UnitTests/Services/QueryParser_Tests.cs @@ -100,7 +100,6 @@ public void Filters_Properly_Parses_DateTime_Without_Operation() // assert Assert.Equal(dt, querySet.Filters.Single(f => f.Attribute == "key").Value); - Assert.Equal(string.Empty, querySet.Filters.Single(f => f.Attribute == "key").Operation); Assert.Equal(FilterOperations.eq, querySet.Filters.Single(f => f.Attribute == "key").OperationType); } From 188de7d39b79a1aaa0b54b7f466c682a99151331 Mon Sep 17 00:00:00 2001 From: Milos Date: Sun, 30 Sep 2018 21:58:45 +0200 Subject: [PATCH 09/18] Fix sort and remove abstract BaseFilterQuery --- src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs | 8 ++++---- src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index 8fd6e2b9d1..8f3b697a6c 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -68,8 +68,8 @@ public static IOrderedQueryable Sort(this IOrderedQueryable Sort(this IQueryable source, IJsonApiContext jsonApiContext, List sortQueries) @@ -120,8 +120,8 @@ public static IOrderedQueryable Sort(this IOrderedQueryable OrderBy(this IQueryable source, string propertyName) diff --git a/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs index 49da22ed84..cf09e0f8ea 100644 --- a/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs @@ -4,7 +4,7 @@ namespace JsonApiDotNetCore.Internal.Query { - public abstract class BaseFilterQuery : BaseAttrQuery + public class BaseFilterQuery : BaseAttrQuery { public BaseFilterQuery( IJsonApiContext jsonApiContext, From 43071ed37be85322fdd556de58f864e910a68bd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20Loub?= Date: Tue, 9 Oct 2018 08:15:30 +0200 Subject: [PATCH 10/18] Fix: filter divided by comma --- src/JsonApiDotNetCore/Services/QueryParser.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/JsonApiDotNetCore/Services/QueryParser.cs b/src/JsonApiDotNetCore/Services/QueryParser.cs index f4f5982cc5..1f8e4f9006 100644 --- a/src/JsonApiDotNetCore/Services/QueryParser.cs +++ b/src/JsonApiDotNetCore/Services/QueryParser.cs @@ -93,7 +93,7 @@ protected virtual List ParseFilterQuery(string key, string value) var values = value.Split(QueryConstants.COMMA); foreach (var val in values) { - var opVal = ParseFilterOperationAndValue(value); + var opVal = ParseFilterOperationAndValue(val); queries.Add(new FilterQuery(propertyName, opVal.value, opVal.operation)); } } @@ -256,7 +256,7 @@ protected virtual List ParseFieldsQuery(string key, string value) return includedFields; } - [Obsolete("Delete also when " + nameof(ParseFilterOperation) + " deleted." )] + [Obsolete("Delete also when " + nameof(ParseFilterOperation) + " deleted.")] private string GetFilterOperationOld(string value) { var values = value.Split(QueryConstants.COLON); From 2e304cc4df6c62e9e7873d3e8229da3460e9bfe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20Loub?= Date: Tue, 9 Oct 2018 08:28:57 +0200 Subject: [PATCH 11/18] Fix: branche conficts in DefaultEntityRepository --- .../Data/DefaultEntityRepository.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index c0c7c93192..969ff8abab 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -90,10 +90,10 @@ public virtual IQueryable Get() /// public virtual IQueryable Filter(IQueryable 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); } @@ -106,15 +106,15 @@ public virtual IQueryable Filter(IQueryable entities, FilterQu public virtual IQueryable Sort(IQueryable entities, List 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)); } @@ -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); } } @@ -289,7 +289,7 @@ public virtual async Task DeleteAsync(TId id) /// public virtual IQueryable Include(IQueryable 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('.'); @@ -297,7 +297,7 @@ public virtual IQueryable Include(IQueryable entities, string // 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); @@ -315,8 +315,8 @@ public virtual IQueryable Include(IQueryable entities, string internalRelationshipPath = (internalRelationshipPath == null) ? relationship.RelationshipPath : $"{internalRelationshipPath}.{relationship.RelationshipPath}"; - - if(i < relationshipChain.Length) + + if (i < relationshipChain.Length) entity = _jsonApiContext.ContextGraph.GetContextEntity(relationship.Type); } From d97affd130cdc1870bc79fc5f00763b8367988e4 Mon Sep 17 00:00:00 2001 From: Milos Date: Mon, 15 Oct 2018 10:25:42 +0200 Subject: [PATCH 12/18] Requested changes --- .../Internal/Query/AttrFilterQuery.cs | 9 ++------- .../Internal/Query/AttrSortQuery.cs | 5 +---- .../Internal/Query/BaseAttrQuery.cs | 12 +++++++----- .../Internal/Query/BaseFilterQuery.cs | 14 +++++--------- .../Internal/Query/FilterQuery.cs | 2 +- .../Internal/Query/RelatedAttrFilterQuery.cs | 7 +++---- .../Internal/Query/RelatedAttrSortQuery.cs | 5 +---- src/JsonApiDotNetCore/Services/QueryParser.cs | 8 +++----- 8 files changed, 23 insertions(+), 39 deletions(-) diff --git a/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs index 575472af06..f415ad9434 100644 --- a/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; @@ -10,11 +9,7 @@ public class AttrFilterQuery : BaseFilterQuery public AttrFilterQuery( IJsonApiContext jsonApiContext, FilterQuery filterQuery) - : base(jsonApiContext, - null, - filterQuery.Attribute, - filterQuery.Value, - filterQuery.OperationType) + : base(jsonApiContext, filterQuery) { if (Attribute == null) throw new JsonApiException(400, $"'{filterQuery.Attribute}' is not a valid attribute."); @@ -25,7 +20,7 @@ public AttrFilterQuery( FilteredAttribute = Attribute; } - [Obsolete("Use " + nameof(Attribute) + " property of " + nameof(BaseAttrQuery) + "class. This property is shared for all AttrQuery and RelatedAttrQuery (filter,sort..) implementations.")] + [Obsolete("Use " + nameof(BaseAttrQuery.Attribute) + " insetad.")] public AttrAttribute FilteredAttribute { get; set; } } } diff --git a/src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs b/src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs index 9243f05eef..341b7e15c0 100644 --- a/src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs @@ -1,6 +1,3 @@ -using System; -using System.Linq; -using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; namespace JsonApiDotNetCore.Internal.Query @@ -10,7 +7,7 @@ public class AttrSortQuery : BaseAttrQuery public AttrSortQuery( IJsonApiContext jsonApiContext, SortQuery sortQuery) - :base(jsonApiContext, null, sortQuery.Attribute) + :base(jsonApiContext, sortQuery) { if (Attribute == null) throw new JsonApiException(400, $"'{sortQuery.Attribute}' is not a valid attribute."); diff --git a/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs b/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs index 3ffff039e1..65cc36d301 100644 --- a/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs @@ -13,15 +13,17 @@ public abstract class BaseAttrQuery { private readonly IJsonApiContext _jsonApiContext; - public BaseAttrQuery(IJsonApiContext jsonApiContext, string relationship, string attribute) + public BaseAttrQuery(IJsonApiContext jsonApiContext, BaseQuery baseQuery) { _jsonApiContext = jsonApiContext; - if (string.IsNullOrEmpty(relationship)) - Attribute = GetAttribute(attribute); + if (baseQuery.IsAttributeOfRelationship) + { + Relationship = GetRelationship(baseQuery.Relationship); + Attribute = GetAttribute(Relationship, baseQuery.Attribute); + } else { - Relationship = GetRelationship(relationship); - Attribute = GetAttribute(Relationship, attribute); + Attribute = GetAttribute(baseQuery.Attribute); } } diff --git a/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs index cf09e0f8ea..d803911a68 100644 --- a/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs @@ -1,4 +1,3 @@ -using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using System; @@ -8,17 +7,14 @@ public class BaseFilterQuery : BaseAttrQuery { public BaseFilterQuery( IJsonApiContext jsonApiContext, - string relationship, - string attribute, - string value, - FilterOperations op) - : base(jsonApiContext, relationship, attribute) + FilterQuery filterQuery) + : base(jsonApiContext, filterQuery) { - PropertyValue = value; - FilterOperation = op; + PropertyValue = filterQuery.Value; + FilterOperation = filterQuery.OperationType; } - [Obsolete("To resolve operation use enum typed " + nameof(FilterQuery.OperationType) + " property of "+ nameof(FilterQuery) +" class")] + [Obsolete("Use " + nameof(FilterQuery.OperationType) + " instead.")] protected FilterOperations GetFilterOperation(string prefix) { if (prefix.Length == 0) return FilterOperations.eq; diff --git a/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs index 178ca5ca5a..135c0ecc22 100644 --- a/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs @@ -31,7 +31,7 @@ public FilterQuery(string attribute, string value, FilterOperations operationTyp [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 Value { get; set; } - [Obsolete("Operation has been replaced by '" + nameof(OperationType) + "'. OperationType is typed enum value for Operation property. This should be default property for providing operation type, because of unsustainable string (not typed) value.")] + [Obsolete("Use '" + nameof(OperationType) + "' instead.")] public string Operation { get; set; } public FilterOperations OperationType { get; set; } diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs index 56d13ce98c..96d33d0e72 100644 --- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; @@ -10,7 +9,7 @@ public class RelatedAttrFilterQuery : BaseFilterQuery public RelatedAttrFilterQuery( IJsonApiContext jsonApiContext, FilterQuery filterQuery) - :base(jsonApiContext, filterQuery.Relationship, filterQuery.Attribute, filterQuery.Value, filterQuery.OperationType) + :base(jsonApiContext, filterQuery) { if (Relationship == null) throw new JsonApiException(400, $"{filterQuery.Relationship} is not a valid relationship on {jsonApiContext.RequestEntity.EntityName}."); @@ -25,10 +24,10 @@ public RelatedAttrFilterQuery( FilteredAttribute = Attribute; } - [Obsolete("Use " + nameof(Attribute) + " property. It's shared for all implementations of BaseAttrQuery(better sort, filter) handling")] + [Obsolete("Use " + nameof(Attribute) + " instead.")] public AttrAttribute FilteredAttribute { get; set; } - [Obsolete("Use " + nameof(Relationship) + " property. It's shared for all implementations of BaseAttrQuery(better sort, filter) handling")] + [Obsolete("Use " + nameof(Relationship) + " instead.")] public RelationshipAttribute FilteredRelationship { get; set; } } } diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrSortQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrSortQuery.cs index aea411f569..4215382c80 100644 --- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrSortQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrSortQuery.cs @@ -1,6 +1,3 @@ -using System; -using System.Linq; -using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; namespace JsonApiDotNetCore.Internal.Query @@ -10,7 +7,7 @@ public class RelatedAttrSortQuery : BaseAttrQuery public RelatedAttrSortQuery( IJsonApiContext jsonApiContext, SortQuery sortQuery) - :base(jsonApiContext, sortQuery.Relationship, sortQuery.Attribute) + :base(jsonApiContext, sortQuery) { if (Relationship == null) throw new JsonApiException(400, $"{sortQuery.Relationship} is not a valid relationship on {jsonApiContext.RequestEntity.EntityName}."); diff --git a/src/JsonApiDotNetCore/Services/QueryParser.cs b/src/JsonApiDotNetCore/Services/QueryParser.cs index 1f8e4f9006..a34ed3d687 100644 --- a/src/JsonApiDotNetCore/Services/QueryParser.cs +++ b/src/JsonApiDotNetCore/Services/QueryParser.cs @@ -101,8 +101,7 @@ protected virtual List ParseFilterQuery(string key, string value) return queries; } - [Obsolete("This method is not used anymore! Use " + nameof(ParseFilterOperationAndValue) + " method with FilterOperations operation return value." + - "Operation as string is not used at all.")] + [Obsolete("Use " + nameof(ParseFilterOperationAndValue) + " method instead.")] protected virtual (string operation, string value) ParseFilterOperation(string value) { if (value.Length < 3) @@ -129,7 +128,7 @@ protected virtual (string operation, string value) ParseFilterOperation(string v /// /// /// - public static (FilterOperations operation, string value) ParseFilterOperationAndValue(string input) + protected virtual (FilterOperations operation, string value) ParseFilterOperationAndValue(string input) { // value is empty if (input.Length == 0) @@ -170,7 +169,7 @@ public static (FilterOperations operation, string value) ParseFilterOperationAnd /// /// String represented operation /// - public static (FilterOperations operation, bool succeeded) GetFilterOperation(string operation) + private static (FilterOperations operation, bool succeeded) GetFilterOperation(string operation) { var success = Enum.TryParse(operation, out FilterOperations opertion); return (opertion, success); @@ -256,7 +255,6 @@ protected virtual List ParseFieldsQuery(string key, string value) return includedFields; } - [Obsolete("Delete also when " + nameof(ParseFilterOperation) + " deleted.")] private string GetFilterOperationOld(string value) { var values = value.Split(QueryConstants.COLON); From 47d4fcd1d73b71b998d4e9ee4954bac8dbf9d2d1 Mon Sep 17 00:00:00 2001 From: Milos Date: Mon, 15 Oct 2018 10:35:04 +0200 Subject: [PATCH 13/18] Fix conflicts --- src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs | 2 +- src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 969ff8abab..77446b3add 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -316,7 +316,7 @@ public virtual IQueryable Include(IQueryable entities, string ? relationship.RelationshipPath : $"{internalRelationshipPath}.{relationship.RelationshipPath}"; - if (i < relationshipChain.Length) + if(i < relationshipChain.Length) entity = _jsonApiContext.ContextGraph.GetContextEntity(relationship.Type); } diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs index 96d33d0e72..7810f46cac 100644 --- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs @@ -29,5 +29,6 @@ public RelatedAttrFilterQuery( [Obsolete("Use " + nameof(Relationship) + " instead.")] public RelationshipAttribute FilteredRelationship { get; set; } + } } From cc3564d21b5244803922e6703bb5d3750483c154 Mon Sep 17 00:00:00 2001 From: Milos Date: Mon, 15 Oct 2018 11:07:36 +0200 Subject: [PATCH 14/18] Fix? --- src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs index 7810f46cac..948f4252a4 100644 --- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs @@ -26,9 +26,7 @@ public RelatedAttrFilterQuery( [Obsolete("Use " + nameof(Attribute) + " instead.")] public AttrAttribute FilteredAttribute { get; set; } - [Obsolete("Use " + nameof(Relationship) + " instead.")] public RelationshipAttribute FilteredRelationship { get; set; } - } } From 5b6e614cf806384b2c38ab164e795800280ae48c Mon Sep 17 00:00:00 2001 From: Milos Date: Mon, 15 Oct 2018 11:08:52 +0200 Subject: [PATCH 15/18] Strange conflict fix --- src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs index 948f4252a4..96d33d0e72 100644 --- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs @@ -26,6 +26,7 @@ public RelatedAttrFilterQuery( [Obsolete("Use " + nameof(Attribute) + " instead.")] public AttrAttribute FilteredAttribute { get; set; } + [Obsolete("Use " + nameof(Relationship) + " instead.")] public RelationshipAttribute FilteredRelationship { get; set; } } From 04172a40a59eac271a819e2646b0db2c0c16c104 Mon Sep 17 00:00:00 2001 From: Milos Date: Mon, 15 Oct 2018 11:25:12 +0200 Subject: [PATCH 16/18] Fix: ContextGraph => ResourceGraph --- src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs b/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs index 65cc36d301..7520f2ebc9 100644 --- a/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs @@ -48,7 +48,7 @@ private RelationshipAttribute GetRelationship(string propertyName) private AttrAttribute GetAttribute(RelationshipAttribute relationship, string attribute) { - var relatedContextExntity = _jsonApiContext.ContextGraph.GetContextEntity(relationship.Type); + var relatedContextExntity = _jsonApiContext.ResourceGraph.GetContextEntity(relationship.Type); return relatedContextExntity.Attributes .FirstOrDefault(a => a.Is(attribute)); } From 0be330008371d4f07050e63c9e34665eb369e5f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20Loub?= Date: Wed, 17 Oct 2018 00:06:15 +0200 Subject: [PATCH 17/18] Revert filter back --- .../Extensions/IQueryableExtensions.cs | 7 +- .../Internal/Query/BaseFilterQuery.cs | 10 +- .../Internal/Query/FilterQuery.cs | 19 +-- .../Services/QueryComposer.cs | 3 +- src/JsonApiDotNetCore/Services/QueryParser.cs | 113 +++++------------- .../Acceptance/Spec/SparseFieldSetTests.cs | 12 +- test/UnitTests/Services/QueryComposerTests.cs | 6 +- test/UnitTests/Services/QueryParser_Tests.cs | 7 +- 8 files changed, 56 insertions(+), 121 deletions(-) diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index 8f3b697a6c..040fad67b5 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -5,7 +5,6 @@ using System.Reflection; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Query; -using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; namespace JsonApiDotNetCore.Extensions @@ -122,7 +121,7 @@ public static IOrderedQueryable Sort(this IOrderedQueryable OrderBy(this IQueryable source, string propertyName) => CallGenericOrderMethod(source, propertyName, "OrderBy"); @@ -183,7 +182,7 @@ public static IQueryable Filter(this IQueryable sourc return source; if (filterQuery.FilterOperation == FilterOperations.@in || filterQuery.FilterOperation == FilterOperations.nin) - return CallGenericWhereContainsMethod(source,filterQuery); + return CallGenericWhereContainsMethod(source, filterQuery); else return CallGenericWhereMethod(source, filterQuery); } @@ -216,7 +215,7 @@ private static Expression GetFilterExpressionLambda(Expression left, Expression case FilterOperations.like: body = Expression.Call(left, "Contains", null, right); break; - // {model.Id != 1} + // {model.Id != 1} case FilterOperations.ne: body = Expression.NotEqual(left, right); break; diff --git a/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs index d803911a68..f7e308369e 100644 --- a/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs @@ -11,11 +11,13 @@ public BaseFilterQuery( : base(jsonApiContext, filterQuery) { PropertyValue = filterQuery.Value; - FilterOperation = filterQuery.OperationType; + FilterOperation = GetFilterOperation(filterQuery.Operation); } - [Obsolete("Use " + nameof(FilterQuery.OperationType) + " instead.")] - protected FilterOperations GetFilterOperation(string prefix) + public string PropertyValue { get; } + public FilterOperations FilterOperation { get; } + + private FilterOperations GetFilterOperation(string prefix) { if (prefix.Length == 0) return FilterOperations.eq; @@ -25,7 +27,5 @@ protected FilterOperations GetFilterOperation(string prefix) return opertion; } - public string PropertyValue { get; } - public FilterOperations FilterOperation { get; } } } diff --git a/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs index 135c0ecc22..220f35b506 100644 --- a/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs @@ -6,35 +6,18 @@ namespace JsonApiDotNetCore.Internal.Query { public class FilterQuery : BaseQuery { - [Obsolete("Use constructor with FilterOperations operationType paremeter. Filter operation should be provided " + - "as enum type, not by string.")] public FilterQuery(string attribute, string value, string operation) - :base(attribute) - { - Key = attribute.ToProperCase(); - Value = value; - Operation = operation; - - Enum.TryParse(operation, out FilterOperations opertion); - OperationType = opertion; - } - - public FilterQuery(string attribute, string value, FilterOperations operationType) : base(attribute) { Key = attribute.ToProperCase(); Value = value; - Operation = operationType.ToString(); - OperationType = operationType; + 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 Value { get; set; } - [Obsolete("Use '" + nameof(OperationType) + "' instead.")] public string Operation { get; set; } - public FilterOperations OperationType { get; set; } - } } diff --git a/src/JsonApiDotNetCore/Services/QueryComposer.cs b/src/JsonApiDotNetCore/Services/QueryComposer.cs index 28d7c927fb..e365811704 100644 --- a/src/JsonApiDotNetCore/Services/QueryComposer.cs +++ b/src/JsonApiDotNetCore/Services/QueryComposer.cs @@ -30,7 +30,8 @@ public string Compose(IJsonApiContext jsonApiContext) private string ComposeSingleFilter(FilterQuery query) { var result = "&filter"; - result += QueryConstants.OPEN_BRACKET + query.Attribute + QueryConstants.CLOSE_BRACKET + "=" + query.OperationType + QueryConstants.COLON + query.Value; + var operation = string.IsNullOrWhiteSpace(query.Operation) ? query.Operation : query.Operation + ":"; + result += QueryConstants.OPEN_BRACKET + query.Attribute + QueryConstants.CLOSE_BRACKET + "=" + operation + query.Value; return result; } } diff --git a/src/JsonApiDotNetCore/Services/QueryParser.cs b/src/JsonApiDotNetCore/Services/QueryParser.cs index a34ed3d687..f559679f33 100644 --- a/src/JsonApiDotNetCore/Services/QueryParser.cs +++ b/src/JsonApiDotNetCore/Services/QueryParser.cs @@ -85,29 +85,32 @@ protected virtual List ParseFilterQuery(string key, string value) var propertyName = key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1]; // InArray case - var arrOpVal = ParseFilterOperationAndValue(value); - if (arrOpVal.operation == FilterOperations.@in || arrOpVal.operation == FilterOperations.nin) - queries.Add(new FilterQuery(propertyName, arrOpVal.value, arrOpVal.operation)); + string op = GetFilterOperation(value); + if (string.Equals(op, FilterOperations.@in.ToString(), StringComparison.OrdinalIgnoreCase) + || string.Equals(op, FilterOperations.nin.ToString(), StringComparison.OrdinalIgnoreCase)) + { + (var operation, var filterValue) = ParseFilterOperation(value); + queries.Add(new FilterQuery(propertyName, filterValue, op)); + } else { var values = value.Split(QueryConstants.COMMA); foreach (var val in values) { - var opVal = ParseFilterOperationAndValue(val); - queries.Add(new FilterQuery(propertyName, opVal.value, opVal.operation)); + (var operation, var filterValue) = ParseFilterOperation(val); + queries.Add(new FilterQuery(propertyName, filterValue, operation)); } } return queries; } - [Obsolete("Use " + nameof(ParseFilterOperationAndValue) + " method instead.")] protected virtual (string operation, string value) ParseFilterOperation(string value) { if (value.Length < 3) return (string.Empty, value); - var operation = GetFilterOperationOld(value); + var operation = GetFilterOperation(value); var values = value.Split(QueryConstants.COLON); if (string.IsNullOrEmpty(operation)) @@ -118,63 +121,6 @@ protected virtual (string operation, string value) ParseFilterOperation(string v return (operation, value); } - /// - /// Parse filter operation enum and value by string value. - /// Input string can contain: - /// a) property value only, then FilterOperations.eq and value is returned - /// b) filter prefix and value e.g. "prefix:value", then FilterOperations.prefix and value is returned - /// In case of prefix is provided and is not in FilterOperations enum, - /// invalid filter prefix exception is thrown. - /// - /// - /// - protected virtual (FilterOperations operation, string value) ParseFilterOperationAndValue(string input) - { - // value is empty - if (input.Length == 0) - return (FilterOperations.eq, input); - - // split value - var values = input.Split(QueryConstants.COLON); - // value only - if (values.Length == 1) - return (FilterOperations.eq, input); - // prefix:value - else if (values.Length == 2) - { - var (operation, succeeded) = GetFilterOperation(values[0]); - if (succeeded == false) - throw new JsonApiException(400, $"Invalid filter prefix '{values[0]}'"); - - return (operation, values[1]); - } - // some:colon:value OR prefix:some:colon:value (datetime) - else - { - // succeeded = false means no prefix found => some value with colons(datetime) - // succeeded = true means prefix provide + some value with colons(datetime) - var (operation, succeeded) = GetFilterOperation(values[0]); - var value = ""; - // datetime - if (succeeded == false) - value = string.Join(QueryConstants.COLON_STR, values); - else - value = string.Join(QueryConstants.COLON_STR, values.Skip(1)); - return (operation, value); - } - } - - /// - /// Returns typed operation result and info about parsing success - /// - /// String represented operation - /// - private static (FilterOperations operation, bool succeeded) GetFilterOperation(string operation) - { - var success = Enum.TryParse(operation, out FilterOperations opertion); - return (opertion, success); - } - protected virtual PageQuery ParsePageQuery(PageQuery pageQuery, string key, string value) { // expected input = page[size]=10 @@ -247,7 +193,12 @@ protected virtual List ParseFieldsQuery(string key, string value) var fields = value.Split(QueryConstants.COMMA); foreach (var field in fields) { - var attr = GetAttribute(field); + var attr = _controllerContext.RequestEntity + .Attributes + .SingleOrDefault(a => a.Is(field)); + + if (attr == null) throw new JsonApiException(400, $"'{_controllerContext.RequestEntity.EntityName}' does not contain '{field}'."); + var internalAttrName = attr.InternalAttributeName; includedFields.Add(internalAttrName); } @@ -255,7 +206,22 @@ protected virtual List ParseFieldsQuery(string key, string value) return includedFields; } - private string GetFilterOperationOld(string value) + protected virtual AttrAttribute GetAttribute(string propertyName) + { + try + { + return _controllerContext + .RequestEntity + .Attributes + .Single(attr => attr.Is(propertyName)); + } + catch (InvalidOperationException e) + { + throw new JsonApiException(400, $"Attribute '{propertyName}' does not exist on resource '{_controllerContext.RequestEntity.EntityName}'", e); + } + } + + private string GetFilterOperation(string value) { var values = value.Split(QueryConstants.COLON); @@ -269,20 +235,5 @@ private string GetFilterOperationOld(string value) return operation; } - - protected virtual AttrAttribute GetAttribute(string attribute) - { - try - { - return _controllerContext - .RequestEntity - .Attributes - .Single(attr => attr.Is(attribute)); - } - catch (InvalidOperationException e) - { - throw new JsonApiException(400, $"Attribute '{attribute}' does not exist on resource '{_controllerContext.RequestEntity.EntityName}'", e); - } - } } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs index a07fe79201..01ee8bd4d6 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs @@ -35,7 +35,8 @@ public async Task Can_Select_Sparse_Fieldsets() { // arrange var fields = new List { "Id", "Description", "CreatedDate", "AchievedDate" }; - var todoItem = new TodoItem { + var todoItem = new TodoItem + { Description = "description", Ordinal = 1, CreatedDate = DateTime.Now, @@ -50,7 +51,7 @@ public async Task Can_Select_Sparse_Fieldsets() // act var query = _dbContext .TodoItems - .Where(t=>t.Id == todoItem.Id) + .Where(t => t.Id == todoItem.Id) .Select(fields); var resultSql = StringExtensions.Normalize(query.ToSql()); @@ -68,14 +69,15 @@ public async Task Can_Select_Sparse_Fieldsets() public async Task Fields_Query_Selects_Sparse_Field_Sets() { // arrange - var todoItem = new TodoItem { + var todoItem = new TodoItem + { Description = "description", - Ordinal = 1, + Ordinal = 1, CreatedDate = DateTime.Now }; _dbContext.TodoItems.Add(todoItem); await _dbContext.SaveChangesAsync(); - + var builder = new WebHostBuilder() .UseStartup(); var httpMethod = new HttpMethod("GET"); diff --git a/test/UnitTests/Services/QueryComposerTests.cs b/test/UnitTests/Services/QueryComposerTests.cs index 0a341c1c99..efa600f2f3 100644 --- a/test/UnitTests/Services/QueryComposerTests.cs +++ b/test/UnitTests/Services/QueryComposerTests.cs @@ -23,7 +23,7 @@ public void Can_ComposeEqual_FilterStringForUrl() var querySet = new QuerySet(); List filters = new List(); filters.Add(filter); - querySet.Filters=filters; + querySet.Filters = filters; _jsonApiContext .Setup(m => m.QuerySet) @@ -56,7 +56,7 @@ public void Can_ComposeLessThan_FilterStringForUrl() // act var filterString = queryComposer.Compose(_jsonApiContext.Object); // assert - Assert.Equal("&filter[attribute]=le:value&filter[attribute2]=eq:value2", filterString); + Assert.Equal("&filter[attribute]=le:value&filter[attribute2]=value2", filterString); } [Fact] @@ -73,7 +73,7 @@ public void NoFilter_Compose_EmptyStringReturned() // act var filterString = queryComposer.Compose(_jsonApiContext.Object); // assert - Assert.Equal("", filterString); + Assert.Equal("", filterString); } } } diff --git a/test/UnitTests/Services/QueryParser_Tests.cs b/test/UnitTests/Services/QueryParser_Tests.cs index 5cd2de16ce..baaee8f81c 100644 --- a/test/UnitTests/Services/QueryParser_Tests.cs +++ b/test/UnitTests/Services/QueryParser_Tests.cs @@ -3,7 +3,6 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Query; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Http; @@ -100,7 +99,7 @@ public void Filters_Properly_Parses_DateTime_Without_Operation() // assert Assert.Equal(dt, querySet.Filters.Single(f => f.Attribute == "key").Value); - Assert.Equal(FilterOperations.eq, querySet.Filters.Single(f => f.Attribute == "key").OperationType); + Assert.Equal(string.Empty, querySet.Filters.Single(f => f.Attribute == "key").Operation); } [Fact] @@ -247,7 +246,7 @@ public void Can_Parse_Fields_Query() .Returns(new ContextEntity { EntityName = type, - Attributes = new List + Attributes = new List { new AttrAttribute(attrName) { @@ -286,7 +285,7 @@ public void Throws_JsonApiException_If_Field_DoesNotExist() .Returns(new ContextEntity { EntityName = type, - Attributes = new List() + Attributes = new List() }); var queryParser = new QueryParser(_controllerContextMock.Object, new JsonApiOptions()); From d1bc2cb2cadb221e252227a843f7503e33f57638 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Wed, 17 Oct 2018 21:19:03 -0700 Subject: [PATCH 18/18] finish deprecating old APIs --- .../Data/DefaultEntityRepository.cs | 2 +- .../Extensions/IQueryableExtensions.cs | 56 ++----------------- .../Internal/Query/SortQuery.cs | 19 ++++--- 3 files changed, 17 insertions(+), 60 deletions(-) diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 520e411982..b92667af95 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -116,7 +116,7 @@ public virtual IQueryable Sort(IQueryable entities, List Sort(this IQueryable source, List sortQueries) - { - if (sortQueries == null || sortQueries.Count == 0) - return source; - - var orderedEntities = source.Sort(sortQueries[0]); - - if (sortQueries.Count <= 1) - return orderedEntities; - - for (var i = 1; i < sortQueries.Count; i++) - orderedEntities = orderedEntities.Sort(sortQueries[i]); - - return orderedEntities; - } - - [Obsolete("Use Sort method with IJsonApiContext parameter instead. New Sort method provides nested sorting.")] - public static IOrderedQueryable Sort(this IQueryable source, SortQuery sortQuery) - { - // For clients using SortQuery constructor with string based parameter - if (sortQuery.SortedAttribute == null) - throw new JsonApiException(400, $"It's not possible to provide {nameof(SortQuery)} without {nameof(SortQuery.SortedAttribute)} parameter." + - $" Use Sort method with IJsonApiContext parameter instead."); + [Obsolete("Use overload Sort(IJsonApiContext, List) instead.", error: true)] + public static IQueryable Sort(this IQueryable source, List sortQueries) => null; - return sortQuery.Direction == SortDirection.Descending - ? source.OrderByDescending(sortQuery.SortedAttribute.InternalAttributeName) - : source.OrderBy(sortQuery.SortedAttribute.InternalAttributeName); - } + [Obsolete("Use overload Sort(IJsonApiContext, SortQuery) instead.", error: true)] + public static IOrderedQueryable Sort(this IQueryable source, SortQuery sortQuery) => null; - [Obsolete("Use Sort method with IJsonApiContext parameter instead. New Sort method provides nested sorting.")] - public static IOrderedQueryable Sort(this IOrderedQueryable source, SortQuery sortQuery) - { - // For clients using SortQuery constructor with string based parameter - if (sortQuery.SortedAttribute == null) - throw new JsonApiException(400, $"It's not possible to provide {nameof(SortQuery)} without {nameof(SortQuery.SortedAttribute)} parameter." + - $" Use Sort method with IJsonApiContext parameter instead."); - - return sortQuery.Direction == SortDirection.Descending - ? source.ThenByDescending(sortQuery.SortedAttribute.InternalAttributeName) - : source.ThenBy(sortQuery.SortedAttribute.InternalAttributeName); - } + [Obsolete("Use overload Sort(IJsonApiContext, SortQuery) instead.", error: true)] + public static IOrderedQueryable Sort(this IOrderedQueryable source, SortQuery sortQuery) => null; public static IQueryable Sort(this IQueryable source, IJsonApiContext jsonApiContext, List sortQueries) { @@ -89,11 +55,6 @@ public static IQueryable Sort(this IQueryable source, public static IOrderedQueryable Sort(this IQueryable source, IJsonApiContext jsonApiContext, SortQuery sortQuery) { - // For clients using constructor with AttrAttribute parameter - if (sortQuery.SortedAttribute != null) - throw new JsonApiException(400, $"It's not possible to provide {nameof(SortQuery)} with {nameof(SortQuery.SortedAttribute)} parameter." + - $" Use {nameof(SortQuery)} constructor overload based on string attribute."); - BaseAttrQuery attr; if (sortQuery.IsAttributeOfRelationship) attr = new RelatedAttrSortQuery(jsonApiContext, sortQuery); @@ -107,11 +68,6 @@ public static IOrderedQueryable Sort(this IQueryable public static IOrderedQueryable Sort(this IOrderedQueryable source, IJsonApiContext jsonApiContext, SortQuery sortQuery) { - // For clients using constructor with AttrAttribute parameter - if (sortQuery.SortedAttribute != null) - throw new JsonApiException(400, $"It's not possible to provide {nameof(SortQuery)} with {nameof(SortQuery.SortedAttribute)} parameter." + - $" Use {nameof(SortQuery)} constructor overload based on string attribute."); - BaseAttrQuery attr; if (sortQuery.IsAttributeOfRelationship) attr = new RelatedAttrSortQuery(jsonApiContext, sortQuery); diff --git a/src/JsonApiDotNetCore/Internal/Query/SortQuery.cs b/src/JsonApiDotNetCore/Internal/Query/SortQuery.cs index 728c770e84..034b7bf7fd 100644 --- a/src/JsonApiDotNetCore/Internal/Query/SortQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/SortQuery.cs @@ -3,17 +3,14 @@ namespace JsonApiDotNetCore.Internal.Query { + /// + /// An internal representation of the raw sort query. + /// public class SortQuery : BaseQuery { - [Obsolete("Use constructor with string attribute parameter. New constructor provides nested sort feature.")] + [Obsolete("Use constructor overload (SortDirection, string) instead.", error: true)] public SortQuery(SortDirection direction, AttrAttribute sortedAttribute) - :base(sortedAttribute.InternalAttributeName) - { - Direction = direction; - SortedAttribute = sortedAttribute; - if (SortedAttribute.IsSortable == false) - throw new JsonApiException(400, $"Sort is not allowed for attribute '{SortedAttribute.PublicAttributeName}'."); - } + : base(sortedAttribute.PublicAttributeName) { } public SortQuery(SortDirection direction, string attribute) : base(attribute) @@ -21,8 +18,12 @@ public SortQuery(SortDirection direction, string attribute) Direction = direction; } + /// + /// Direction the sort should be applied + /// public SortDirection Direction { get; set; } - [Obsolete("Use string based Attribute instead. This provides nested sort feature (e.g. ?sort=owner.first-name)")] + + [Obsolete("Use string based Attribute instead.", error: true)] public AttrAttribute SortedAttribute { get; set; } } }