From 33d213fd3ac7521aacabf774a95dbdbfd338ec2f Mon Sep 17 00:00:00 2001 From: Simon Hofer Date: Wed, 28 Sep 2016 19:08:39 +0200 Subject: [PATCH 1/6] added wildcard filtering --- .../DefaultFilteringTransformerTests.cs | 52 +++++++++++++++++++ .../DefaultFilteringTransformer.cs | 49 +++++++++++++++-- 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs index 6b458e2a..e55eb0df 100644 --- a/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs +++ b/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs @@ -582,6 +582,58 @@ public void Filters_by_matching_string_property() returnedArray[0].Id.Should().Be("100"); } + [TestMethod] + public void Filters_by_wildcard_string_property_end() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[string-field]=String value 1%"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("100"); + } + [TestMethod] + public void Filters_by_wildcard_string_property_start() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[string-field]=%String value 1"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("100"); + } + [TestMethod] + public void Filters_by_wildcard_string_property_start_end() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[string-field]=%String value 1%"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("100"); + } + + [TestMethod] + public void Filters_by_wildcard_string_property_start_end_ignoreCase() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[string-field]=%string value 1%"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("100"); + } + + [TestMethod] + public void Filters_by_wildcard_string_property_end_part() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[string-field]=%value 2"); + returnedArray.Length.Should().Be(2); + returnedArray[0].Id.Should().Be("101"); + returnedArray[1].Id.Should().Be("102"); + } + [TestMethod] + public void Filters_by_wildcard_string_property_end_part_no_match() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[string-field]=%value 3"); + returnedArray.Length.Should().Be(0); + } + + [TestMethod] + public void Filters_by_wildcard_string_property_start_part_no_match() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[string-field]=String vall%"); + returnedArray.Length.Should().Be(0); + } + [TestMethod] public void Filters_by_missing_string_property() { diff --git a/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs b/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs index f449d1bb..a9a0d478 100644 --- a/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs +++ b/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs @@ -16,6 +16,13 @@ public class DefaultFilteringTransformer : IQueryableFilteringTransformer { private readonly IResourceTypeRegistry _resourceTypeRegistry; + private static readonly MethodInfo ContainsMethod = typeof(string).GetMethod("Contains"); + private static readonly MethodInfo StartsWithMethod = typeof(string).GetMethod("StartsWith", new[] { typeof(string) }); + private static readonly MethodInfo EndsWithMethod = typeof(string).GetMethod("EndsWith", new[] { typeof(string) }); + private static readonly MethodInfo ToLowerMethod = typeof(string).GetMethod("ToLower", new Type[] {}); + + + /// /// Creates a new FilteringQueryableTransformer /// @@ -135,9 +142,45 @@ private Expression GetPredicateBodyForProperty(PropertyInfo prop, string queryVa expr = Expression.Equal(propertyExpr, Expression.Constant(null)); } else - { - Expression propertyExpr = Expression.Property(param, prop); - expr = Expression.Equal(propertyExpr, Expression.Constant(queryValue)); + { // inspired by http://stackoverflow.com/questions/5374481/like-operator-in-linq + if (queryValue.StartsWith("%") || queryValue.EndsWith("%")) + { + var startWith = queryValue.StartsWith("%"); + var endsWith = queryValue.EndsWith("%"); + + if (startWith) // remove % + queryValue = queryValue.Remove(0, 1); + + if (endsWith) // remove % + queryValue = queryValue.Remove(queryValue.Length - 1, 1); + + var constant = Expression.Constant(queryValue.ToLower()); + Expression propertyExpr = Expression.Property(param, prop); + + Expression nullCheckExpression = Expression.NotEqual(propertyExpr, Expression.Constant(null)); + + if (endsWith && startWith) + { + expr = Expression.AndAlso(nullCheckExpression, Expression.Call(Expression.Call(propertyExpr,ToLowerMethod), ContainsMethod, constant)); + } + else if (startWith) + { + expr = Expression.AndAlso(nullCheckExpression, Expression.Call(Expression.Call(propertyExpr, ToLowerMethod), EndsWithMethod, constant)); + } + else if (endsWith) + { + expr = Expression.AndAlso(nullCheckExpression, Expression.Call(Expression.Call(propertyExpr, ToLowerMethod), StartsWithMethod, constant)); + } + else + { + expr = Expression.Equal(propertyExpr, constant); + } + } + else + { + Expression propertyExpr = Expression.Property(param, prop); + expr = Expression.Equal(propertyExpr, Expression.Constant(queryValue)); + } } } else if (propertyType == typeof(Boolean)) From 819c476d33686168bff5dfa262b93b2a3a4548fc Mon Sep 17 00:00:00 2001 From: Simon Hofer Date: Thu, 29 Sep 2016 11:51:50 +0200 Subject: [PATCH 2/6] made filtering more generic an support multiple values separated by comma --- .../DefaultFilteringTransformerTests.cs | 201 ++++++++- .../DefaultFilteringTransformer.cs | 417 ++++++++---------- 2 files changed, 389 insertions(+), 229 deletions(-) diff --git a/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs index e55eb0df..df49a463 100644 --- a/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs +++ b/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs @@ -22,7 +22,10 @@ private enum SomeEnum { EnumValue1 = 1, EnumValue2 = 2, - EnumValue3 = 3 + EnumValue3 = 3, + EnumValue4 = 4, + EnumValue5 = 5, + EnumValue6 = 6 } private class SomeUnknownType @@ -101,7 +104,26 @@ public void SetupFixtures() StringField = "String value 2", EnumField = SomeEnum.EnumValue3 }, - + new Dummy + { + Id = "103", + StringField = "abc", + }, + new Dummy + { + Id = "104", + StringField = "bcd", + }, + new Dummy + { + Id = "105", + StringField = "def", + }, + new Dummy + { + Id = "106", + StringField = "sentence containing a comma, which can happen", + }, #endregion #region DateTimeField @@ -151,6 +173,11 @@ public void SetupFixtures() Id = "140", NullableDateTimeOffsetField = new DateTime(2014, 5, 5) }, + new Dummy + { + Id = "141", + NullableDateTimeOffsetField = new DateTime(2015, 6, 13) + }, #endregion @@ -166,6 +193,41 @@ public void SetupFixtures() Id = "151", EnumField = SomeEnum.EnumValue2 }, + new Dummy + { + Id = "152", + EnumField = SomeEnum.EnumValue3 + }, + new Dummy + { + Id = "153", + EnumField = SomeEnum.EnumValue4 + }, + new Dummy + { + Id = "154", + EnumField = SomeEnum.EnumValue5 + }, + new Dummy + { + Id = "155", + EnumField = SomeEnum.EnumValue6 + }, + new Dummy + { + Id = "156", + EnumField = SomeEnum.EnumValue5 + }, + new Dummy + { + Id = "157", + EnumField = SomeEnum.EnumValue6 + }, + new Dummy + { + Id = "158", + EnumField = SomeEnum.EnumValue5 + }, #endregion @@ -176,6 +238,11 @@ public void SetupFixtures() Id = "160", NullableEnumField = SomeEnum.EnumValue3 }, + new Dummy + { + Id = "161", + NullableEnumField = SomeEnum.EnumValue6 + }, #endregion @@ -191,6 +258,16 @@ public void SetupFixtures() Id = "171", DecimalField = (decimal) 6.37 }, + new Dummy + { + Id = "172", + DecimalField = (decimal) 5.08 + }, + new Dummy + { + Id = "173", + DecimalField = (decimal) 17.3 + }, #endregion @@ -582,6 +659,30 @@ public void Filters_by_matching_string_property() returnedArray[0].Id.Should().Be("100"); } + [TestMethod] + public void Filters_by_matching_string_property_comma_not_matching() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[string-field]=sentence containing a comma, which can happen"); + returnedArray.Length.Should().Be(0); + } + + [TestMethod] + public void Filters_by_matching_string_property_comma_matching() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[string-field]=abc,bcd"); + returnedArray.Length.Should().Be(2); + returnedArray[0].Id.Should().Be("103"); + returnedArray[1].Id.Should().Be("104"); + } + + [TestMethod] + public void Filters_by_matching_string_property_comma() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[string-field]=\"sentence containing a comma, which can happen\""); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("106"); + } + [TestMethod] public void Filters_by_wildcard_string_property_end() { @@ -604,6 +705,7 @@ public void Filters_by_wildcard_string_property_start_end() returnedArray[0].Id.Should().Be("100"); } + [TestMethod] public void Filters_by_wildcard_string_property_start_end_ignoreCase() { @@ -612,6 +714,14 @@ public void Filters_by_wildcard_string_property_start_end_ignoreCase() returnedArray[0].Id.Should().Be("100"); } + [TestMethod] + public void Filters_by_wildcard_string_property_start_end_comma() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[string-field]=\"%,%\""); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("106"); + } + [TestMethod] public void Filters_by_wildcard_string_property_end_part() { @@ -620,6 +730,25 @@ public void Filters_by_wildcard_string_property_end_part() returnedArray[0].Id.Should().Be("101"); returnedArray[1].Id.Should().Be("102"); } + + [TestMethod] + public void Filters_by_wildcard_string_property_end_part_quote() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[string-field]=\"%value 2\""); + returnedArray.Length.Should().Be(2); + returnedArray[0].Id.Should().Be("101"); + returnedArray[1].Id.Should().Be("102"); + } + + + [TestMethod] + public void Filters_by_wildcard_string_property_end_part_quote_comma() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[string-field]=\"%, which can happen\""); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("106"); + } + [TestMethod] public void Filters_by_wildcard_string_property_end_part_no_match() { @@ -634,12 +763,21 @@ public void Filters_by_wildcard_string_property_start_part_no_match() returnedArray.Length.Should().Be(0); } + [TestMethod] + public void Filters_by_multiple_string_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[string-field]=\"abc\",\"def\""); + returnedArray.Length.Should().Be(2); + returnedArray[0].Id.Should().Be("103"); + returnedArray[1].Id.Should().Be("105"); + } + [TestMethod] public void Filters_by_missing_string_property() { var returnedArray = GetArray("http://api.example.com/dummies?filter[string-field]="); - returnedArray.Length.Should().Be(_fixtures.Count - 3); - returnedArray.Any(d => d.Id == "100" || d.Id == "101" || d.Id == "102").Should().BeFalse(); + returnedArray.Length.Should().Be(_fixtures.Count - 7); + returnedArray.Any(d => d.Id == "100" || d.Id == "101" || d.Id == "102" || d.Id == "103" || d.Id == "104" || d.Id == "105" || d.Id == "106").Should().BeFalse(); } #endregion @@ -704,12 +842,21 @@ public void Filters_by_matching_nullable_datetimeoffset_property() returnedArray[0].Id.Should().Be("140"); } + [TestMethod] + public void Filters_by_multiple_matching_nullable_datetimeoffset_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-date-time-offset-field]=2014-05-05,2015-06-13"); + returnedArray.Length.Should().Be(2); + returnedArray[0].Id.Should().Be("140"); + returnedArray[1].Id.Should().Be("141"); + } + [TestMethod] public void Filters_by_missing_nullable_datetimeoffset_property() { var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-date-time-offset-field]="); - returnedArray.Length.Should().Be(_fixtures.Count - 1); - returnedArray.Any(d => d.Id == "140").Should().BeFalse(); + returnedArray.Length.Should().Be(_fixtures.Count - 2); + returnedArray.Any(d => d.Id == "140" || d.Id == "141").Should().BeFalse(); } #endregion @@ -724,6 +871,26 @@ public void Filters_by_matching_enum_property() returnedArray[0].Id.Should().Be("150"); } + [TestMethod] + public void Filters_by_multiple_matching_enum_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[enum-field]=1,2"); + returnedArray.Length.Should().Be(2); + returnedArray[0].Id.Should().Be("150"); + returnedArray[1].Id.Should().Be("151"); + } + + [TestMethod] + public void Filters_by_multiple_matching_enum_property2() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[enum-field]=1,5"); + returnedArray.Length.Should().Be(4); + returnedArray[0].Id.Should().Be("150"); + returnedArray[1].Id.Should().Be("154"); + returnedArray[2].Id.Should().Be("156"); + returnedArray[3].Id.Should().Be("158"); + } + [TestMethod] public void Filters_by_missing_enum_property() { @@ -739,12 +906,21 @@ public void Filters_by_matching_nullable_enum_property() returnedArray[0].Id.Should().Be("160"); } + [TestMethod] + public void Filters_by_multiple_matching_nullable_enum_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-enum-field]=3,6"); + returnedArray.Length.Should().Be(2); + returnedArray[0].Id.Should().Be("160"); + returnedArray[1].Id.Should().Be("161"); + } + [TestMethod] public void Filters_by_missing_nullable_enum_property() { var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-enum-field]="); - returnedArray.Length.Should().Be(_fixtures.Count - 1); - returnedArray.Any(d => d.Id == "160").Should().BeFalse(); + returnedArray.Length.Should().Be(_fixtures.Count - 2); + returnedArray.Any(d => d.Id == "160" || d.Id == "161").Should().BeFalse(); } #endregion @@ -759,6 +935,15 @@ public void Filters_by_matching_decimal_property() returnedArray[0].Id.Should().Be("170"); } + [TestMethod] + public void Filters_by_matching_multiple_decimal_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[decimal-field]=4.03,5.08"); + returnedArray.Length.Should().Be(2); + returnedArray[0].Id.Should().Be("170"); + returnedArray[1].Id.Should().Be("172"); + } + [TestMethod] public void Filters_by_matching_decimal_property_non_en_US() { diff --git a/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs b/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs index a9a0d478..c72a08dc 100644 --- a/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs +++ b/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs @@ -1,9 +1,11 @@ using System; -using System.Globalization; +using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Linq.Expressions; using System.Net.Http; using System.Reflection; +using System.Text.RegularExpressions; using JSONAPI.Core; using JSONAPI.Documents.Builders; @@ -20,7 +22,7 @@ public class DefaultFilteringTransformer : IQueryableFilteringTransformer private static readonly MethodInfo StartsWithMethod = typeof(string).GetMethod("StartsWith", new[] { typeof(string) }); private static readonly MethodInfo EndsWithMethod = typeof(string).GetMethod("EndsWith", new[] { typeof(string) }); private static readonly MethodInfo ToLowerMethod = typeof(string).GetMethod("ToLower", new Type[] {}); - + private static readonly MethodInfo GetPropertyExpressionMethod = typeof(DefaultFilteringTransformer).GetMethod("GetPropertyExpression", BindingFlags.NonPublic | BindingFlags.Static); /// @@ -127,266 +129,239 @@ private Expression GetPredicateBodyForField(ResourceTypeAttribute resourceTypeAt return GetPredicateBodyForProperty(resourceTypeAttribute.Property, queryValue, param); } - // ReSharper disable once FunctionComplexityOverflow - // TODO: should probably break this method up private Expression GetPredicateBodyForProperty(PropertyInfo prop, string queryValue, ParameterExpression param) { var propertyType = prop.PropertyType; - Expression expr; - if (propertyType == typeof(String)) + Expression expr = null; + if (propertyType == typeof(string)) { - if (String.IsNullOrWhiteSpace(queryValue)) + if (string.IsNullOrWhiteSpace(queryValue)) { Expression propertyExpr = Expression.Property(param, prop); expr = Expression.Equal(propertyExpr, Expression.Constant(null)); } else - { // inspired by http://stackoverflow.com/questions/5374481/like-operator-in-linq - if (queryValue.StartsWith("%") || queryValue.EndsWith("%")) + { + List parts = new List(); + if (queryValue.Contains("\"")) { - var startWith = queryValue.StartsWith("%"); - var endsWith = queryValue.EndsWith("%"); - - if (startWith) // remove % - queryValue = queryValue.Remove(0, 1); - - if (endsWith) // remove % - queryValue = queryValue.Remove(queryValue.Length - 1, 1); - - var constant = Expression.Constant(queryValue.ToLower()); - Expression propertyExpr = Expression.Property(param, prop); - - Expression nullCheckExpression = Expression.NotEqual(propertyExpr, Expression.Constant(null)); - - if (endsWith && startWith) + queryValue= queryValue.Replace("\"\"", "_#quote#_"); // escaped quotes + queryValue = Regex.Replace(queryValue, "\"([^\"]*)\"", delegate (Match match) + { + string v = match.ToString(); + v = v.Trim('"'); + v = v.Replace("_#quote#_", "\""); // restore quotes + parts.Add(v); + return string.Empty; + }); + } + + parts.AddRange(queryValue.Split(',')); + + foreach (var qpart in parts) + { + Expression innerExpression; + // inspired by http://stackoverflow.com/questions/5374481/like-operator-in-linq + if (qpart.StartsWith("%") || qpart.EndsWith("%")) { - expr = Expression.AndAlso(nullCheckExpression, Expression.Call(Expression.Call(propertyExpr,ToLowerMethod), ContainsMethod, constant)); + var startWith = qpart.StartsWith("%"); + var endsWith = qpart.EndsWith("%"); + string innerPart = qpart; + + if (startWith) // remove % + innerPart = innerPart.Remove(0, 1); + + if (endsWith) // remove % + innerPart = innerPart.Remove(innerPart.Length - 1, 1); + + var constant = Expression.Constant(innerPart.ToLower()); + Expression propertyExpr = Expression.Property(param, prop); + + Expression nullCheckExpression = Expression.NotEqual(propertyExpr, Expression.Constant(null)); + + if (endsWith && startWith) + { + innerExpression = Expression.AndAlso(nullCheckExpression, Expression.Call(Expression.Call(propertyExpr, ToLowerMethod), ContainsMethod, constant)); + } + else if (startWith) + { + innerExpression = Expression.AndAlso(nullCheckExpression, Expression.Call(Expression.Call(propertyExpr, ToLowerMethod), EndsWithMethod, constant)); + } + else if (endsWith) + { + innerExpression = Expression.AndAlso(nullCheckExpression, Expression.Call(Expression.Call(propertyExpr, ToLowerMethod), StartsWithMethod, constant)); + } + else + { + innerExpression = Expression.Equal(propertyExpr, constant); + } } - else if (startWith) + else { - expr = Expression.AndAlso(nullCheckExpression, Expression.Call(Expression.Call(propertyExpr, ToLowerMethod), EndsWithMethod, constant)); + Expression propertyExpr = Expression.Property(param, prop); + innerExpression = Expression.Equal(propertyExpr, Expression.Constant(qpart)); } - else if (endsWith) + + if (expr == null) { - expr = Expression.AndAlso(nullCheckExpression, Expression.Call(Expression.Call(propertyExpr, ToLowerMethod), StartsWithMethod, constant)); + expr = innerExpression; } else { - expr = Expression.Equal(propertyExpr, constant); + expr = Expression.OrElse(expr, innerExpression); } } - else - { - Expression propertyExpr = Expression.Property(param, prop); - expr = Expression.Equal(propertyExpr, Expression.Constant(queryValue)); - } + } } - else if (propertyType == typeof(Boolean)) - { - bool value; - expr = bool.TryParse(queryValue, out value) - ? GetPropertyExpression(value, prop, param) - : Expression.Constant(false); - } - else if (propertyType == typeof(Boolean?)) - { - bool tmp; - var value = bool.TryParse(queryValue, out tmp) ? tmp : (bool?)null; - expr = GetPropertyExpression(value, prop, param); - } - else if (propertyType == typeof(SByte)) - { - SByte value; - expr = SByte.TryParse(queryValue, out value) - ? GetPropertyExpression(value, prop, param) - : Expression.Constant(false); - } - else if (propertyType == typeof(SByte?)) - { - SByte tmp; - var value = SByte.TryParse(queryValue, out tmp) ? tmp : (SByte?)null; - expr = GetPropertyExpression(value, prop, param); - } - else if (propertyType == typeof(Byte)) - { - Byte value; - expr = Byte.TryParse(queryValue, out value) - ? GetPropertyExpression(value, prop, param) - : Expression.Constant(false); - } - else if (propertyType == typeof(Byte?)) - { - Byte tmp; - var value = Byte.TryParse(queryValue, out tmp) ? tmp : (Byte?)null; - expr = GetPropertyExpression(value, prop, param); - } - else if (propertyType == typeof(Int16)) - { - Int16 value; - expr = Int16.TryParse(queryValue, out value) - ? GetPropertyExpression(value, prop, param) - : Expression.Constant(false); - } - else if (propertyType == typeof(Int16?)) - { - Int16 tmp; - var value = Int16.TryParse(queryValue, out tmp) ? tmp : (Int16?)null; - expr = GetPropertyExpression(value, prop, param); - } - else if (propertyType == typeof(UInt16)) - { - UInt16 value; - expr = UInt16.TryParse(queryValue, out value) - ? GetPropertyExpression(value, prop, param) - : Expression.Constant(false); - } - else if (propertyType == typeof(UInt16?)) - { - UInt16 tmp; - var value = UInt16.TryParse(queryValue, out tmp) ? tmp : (UInt16?)null; - expr = GetPropertyExpression(value, prop, param); - } - else if (propertyType == typeof(Int32)) - { - Int32 value; - expr = Int32.TryParse(queryValue, out value) - ? GetPropertyExpression(value, prop, param) - : Expression.Constant(false); - } - else if (propertyType == typeof(Int32?)) - { - Int32 tmp; - var value = Int32.TryParse(queryValue, out tmp) ? tmp : (Int32?)null; - expr = GetPropertyExpression(value, prop, param); - } - else if (propertyType == typeof(UInt32)) - { - UInt32 value; - expr = UInt32.TryParse(queryValue, out value) - ? GetPropertyExpression(value, prop, param) - : Expression.Constant(false); - } - else if (propertyType == typeof(UInt32?)) - { - UInt32 tmp; - var value = UInt32.TryParse(queryValue, out tmp) ? tmp : (UInt32?)null; - expr = GetPropertyExpression(value, prop, param); - } - else if (propertyType == typeof(Int64)) - { - Int64 value; - expr = Int64.TryParse(queryValue, out value) - ? GetPropertyExpression(value, prop, param) - : Expression.Constant(false); - } - else if (propertyType == typeof(Int64?)) - { - Int64 tmp; - var value = Int64.TryParse(queryValue, out tmp) ? tmp : (Int64?)null; - expr = GetPropertyExpression(value, prop, param); - } - else if (propertyType == typeof(UInt64)) - { - UInt64 value; - expr = UInt64.TryParse(queryValue, out value) - ? GetPropertyExpression(value, prop, param) - : Expression.Constant(false); - } - else if (propertyType == typeof(UInt64?)) - { - UInt64 tmp; - var value = UInt64.TryParse(queryValue, out tmp) ? tmp : (UInt64?)null; - expr = GetPropertyExpression(value, prop, param); - } - else if (propertyType == typeof(Single)) - { - Single value; - expr = Single.TryParse(queryValue, NumberStyles.Any, CultureInfo.InvariantCulture, out value) - ? GetPropertyExpression(value, prop, param) - : Expression.Constant(false); - } - else if (propertyType == typeof(Single?)) - { - Single tmp; - var value = Single.TryParse(queryValue, NumberStyles.Any, CultureInfo.InvariantCulture, out tmp) ? tmp : (Single?)null; - expr = GetPropertyExpression(value, prop, param); - } - else if (propertyType == typeof(Double)) - { - Double value; - expr = Double.TryParse(queryValue, NumberStyles.Any, CultureInfo.InvariantCulture, out value) - ? GetPropertyExpression(value, prop, param) - : Expression.Constant(false); - } - else if (propertyType == typeof(Double?)) - { - Double tmp; - var value = Double.TryParse(queryValue, NumberStyles.Any, CultureInfo.InvariantCulture, out tmp) ? tmp : (Double?)null; - expr = GetPropertyExpression(value, prop, param); - } - else if (propertyType == typeof(Decimal)) - { - Decimal value; - expr = Decimal.TryParse(queryValue, NumberStyles.Any, CultureInfo.InvariantCulture, out value) - ? GetPropertyExpression(value, prop, param) - : Expression.Constant(false); - } - else if (propertyType == typeof(Decimal?)) + else if (propertyType.IsEnum) { - Decimal tmp; - var value = Decimal.TryParse(queryValue, NumberStyles.Any, CultureInfo.InvariantCulture, out tmp) ? tmp : (Decimal?)null; - expr = GetPropertyExpression(value, prop, param); + if (string.IsNullOrWhiteSpace(queryValue)) // missing enum property + { + expr = Expression.Constant(false); + } + else + { + // try to split up for multiple values + var parts = queryValue.Split(','); + + foreach (var part in parts) + { + int value; + var partExpr = (int.TryParse(part, out value) && Enum.IsDefined(propertyType, value)) + ? GetEnumPropertyExpression(value, prop, param) + : Expression.Constant(false); + if (expr == null) + { + expr = partExpr; + } + else + { + expr = Expression.OrElse(expr, partExpr); + } + } + } } - else if (propertyType == typeof(DateTime)) + else if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>) && + propertyType.GenericTypeArguments[0].IsEnum) { - DateTime value; - expr = DateTime.TryParse(queryValue, out value) - ? GetPropertyExpression(value, prop, param) - : Expression.Constant(false); + if (string.IsNullOrWhiteSpace(queryValue)) + { + Expression propertyExpr = Expression.Property(param, prop); + expr = Expression.Equal(propertyExpr, Expression.Constant(null)); + } + else + { + // try to split up for multiple values + var parts = queryValue.Split(','); + + foreach (var part in parts) + { + int tmp; + var value = int.TryParse(part, out tmp) ? tmp : (int?)null; + var partExpr = GetEnumPropertyExpression(value, prop, param); + if (expr == null) + { + expr = partExpr; + } + else + { + expr = Expression.OrElse(expr, partExpr); + } + } + } } - else if (propertyType == typeof(DateTime?)) + else if (Nullable.GetUnderlyingType(propertyType) != null) // It's nullable { - DateTime tmp; - var value = DateTime.TryParse(queryValue, out tmp) ? tmp : (DateTime?)null; - expr = GetPropertyExpression(value, prop, param); + expr = GetExpressionNullable(queryValue, prop, propertyType, param); } - else if (propertyType == typeof(DateTimeOffset)) + else { - DateTimeOffset value; - expr = DateTimeOffset.TryParse(queryValue, out value) - ? GetPropertyExpression(value, prop, param) - : Expression.Constant(false); + expr = GetExpression(queryValue, prop, propertyType, param); + if (expr == null) + { + expr = Expression.Constant(true); + } } - else if (propertyType == typeof(DateTimeOffset?)) + + return expr; + } + + private Expression GetExpressionNullable(string queryValue, PropertyInfo prop, Type propertyType, ParameterExpression param) + { + Type underlayingType = Nullable.GetUnderlyingType(propertyType); + try { - DateTimeOffset tmp; - var value = DateTimeOffset.TryParse(queryValue, out tmp) ? tmp : (DateTimeOffset?)null; - expr = GetPropertyExpression(value, prop, param); + + var methodInfo = GetPropertyExpressionMethod.MakeGenericMethod(propertyType); + + if (queryValue == null) + { + return (Expression)methodInfo.Invoke(null, new object[] { null, prop, param }); + } + + // try to split up for multiple values + var parts = queryValue.Split(','); + + Expression expr = null; + foreach (var part in parts) + { + TypeConverter conv =TypeDescriptor.GetConverter(underlayingType); + var value = conv.ConvertFromInvariantString(part); + + if (expr == null) + { + expr = (Expression) methodInfo.Invoke(null, new[] { value, prop, param }); + } + else + { + expr = Expression.OrElse(expr, (Expression)methodInfo.Invoke(null, new[] { value, prop, param })); + } + } + return expr; } - else if (propertyType.IsEnum) + catch (NotSupportedException) { - int value; - expr = (int.TryParse(queryValue, out value) && Enum.IsDefined(propertyType, value)) - ? GetEnumPropertyExpression(value, prop, param) - : Expression.Constant(false); + return Expression.Constant(false); } - else if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof (Nullable<>) && - propertyType.GenericTypeArguments[0].IsEnum) + + } + + + + + private Expression GetExpression(string queryValue, PropertyInfo prop, Type propertyType, ParameterExpression param) + { + try { - int tmp; - var value = int.TryParse(queryValue, out tmp) ? tmp : (int?) null; - expr = GetEnumPropertyExpression(value, prop, param); + if(queryValue == null) // missing property + return Expression.Constant(false); + + var parts = queryValue.Split(','); + Expression expr = null; + foreach (var part in parts) + { + dynamic value = TypeDescriptor.GetConverter(propertyType).ConvertFromInvariantString(part); + if (expr == null) + { + expr = GetPropertyExpression(value, prop, param); + } + else + { + expr = Expression.OrElse(expr, GetPropertyExpression(value, prop, param)); + } + } + return expr; } - else + catch (NotSupportedException) { - expr = Expression.Constant(true); + return Expression.Constant(false); } - - return expr; } + + private Expression GetPredicateBodyForRelationship(ResourceTypeRelationship resourceTypeProperty, string queryValue, ParameterExpression param) { var relatedType = resourceTypeProperty.RelatedType; From 37e2e453d7a1d61692b20ad12495bd245c2fcb69 Mon Sep 17 00:00:00 2001 From: Simon Hofer Date: Thu, 29 Sep 2016 16:53:52 +0200 Subject: [PATCH 3/6] added filtering for date ranges (year, month, day, hour, minute, second) --- .../DefaultFilteringTransformerTests.cs | 94 ++++++++++++++++++- .../DefaultFilteringTransformer.cs | 90 +++++++++++++++++- 2 files changed, 181 insertions(+), 3 deletions(-) diff --git a/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs index df49a463..6ab52b65 100644 --- a/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs +++ b/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs @@ -148,6 +148,27 @@ public void SetupFixtures() Id = "120", NullableDateTimeField = new DateTime(1961, 2, 18) }, + new Dummy + { + Id = "121", + NullableDateTimeField = new DateTime(1961, 5, 31) + }, + new Dummy + { + Id = "122", + NullableDateTimeField = new DateTime(1961, 5, 31, 18, 58, 0) + }, + new Dummy + { + Id = "123", + NullableDateTimeField = new DateTime(1961, 5, 31, 19, 01, 0) + }, + + new Dummy + { + Id = "124", + NullableDateTimeField = new DateTime(1962, 5, 31, 19, 01, 0) + }, #endregion @@ -807,12 +828,73 @@ public void Filters_by_matching_nullable_datetime_property() returnedArray[0].Id.Should().Be("120"); } + [TestMethod] + public void Filters_by_matching_nullable_datetime_property_month() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-date-time-field]=1961-02"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("120"); + } + + [TestMethod] + public void Filters_by_multiple_matching_nullable_datetime_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-date-time-field]=1961-02-18,1961-05-31"); + returnedArray.Length.Should().Be(4); + returnedArray[0].Id.Should().Be("120"); + returnedArray[1].Id.Should().Be("121"); + returnedArray[2].Id.Should().Be("122"); + returnedArray[3].Id.Should().Be("123"); + } + + + [TestMethod] + public void Filters_by_multiple_matching_nullable_datetime_property_year() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-date-time-field]=1961"); + returnedArray.Length.Should().Be(4); + returnedArray[0].Id.Should().Be("120"); + returnedArray[1].Id.Should().Be("121"); + returnedArray[2].Id.Should().Be("122"); + returnedArray[3].Id.Should().Be("123"); + } + + [TestMethod] + public void Filters_by_multiple_matching_nullable_datetime_property_month() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-date-time-field]=1961-02"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("120"); + } + + [TestMethod] + public void Filters_by_multiple_matching_nullable_datetime_property_hour() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-date-time-field]=1961-05-31 19"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("123"); + } + + [TestMethod] + public void Filters_by_multiple_matching_nullable_datetime_property_hour_minute() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-date-time-field]=1961-05-31 19:01"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("123"); + } + [TestMethod] + public void Filters_by_multiple_matching_nullable_datetime_property_time_missing() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-date-time-field]=1961-05-31 19:01:12"); + returnedArray.Length.Should().Be(0); + } + [TestMethod] public void Filters_by_missing_nullable_datetime_property() { var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-date-time-field]="); - returnedArray.Length.Should().Be(_fixtures.Count - 1); - returnedArray.Any(d => d.Id == "120").Should().BeFalse(); + returnedArray.Length.Should().Be(_fixtures.Count - 5); + returnedArray.Any(d => d.Id == "120" || d.Id == "121" || d.Id == "122" || d.Id == "123" || d.Id == "124").Should().BeFalse(); } #endregion @@ -842,6 +924,14 @@ public void Filters_by_matching_nullable_datetimeoffset_property() returnedArray[0].Id.Should().Be("140"); } + [TestMethod] + public void Filters_by_matching_nullable_datetimeoffset_property_month() + { + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-date-time-offset-field]=2014-05"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("140"); + } + [TestMethod] public void Filters_by_multiple_matching_nullable_datetimeoffset_property() { diff --git a/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs b/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs index c72a08dc..92196a50 100644 --- a/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs +++ b/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs @@ -23,6 +23,7 @@ public class DefaultFilteringTransformer : IQueryableFilteringTransformer private static readonly MethodInfo EndsWithMethod = typeof(string).GetMethod("EndsWith", new[] { typeof(string) }); private static readonly MethodInfo ToLowerMethod = typeof(string).GetMethod("ToLower", new Type[] {}); private static readonly MethodInfo GetPropertyExpressionMethod = typeof(DefaultFilteringTransformer).GetMethod("GetPropertyExpression", BindingFlags.NonPublic | BindingFlags.Static); + private static readonly MethodInfo GetPropertyExpressionBetweenMethod = typeof(DefaultFilteringTransformer).GetMethod("GetPropertyExpressionBetween", BindingFlags.NonPublic | BindingFlags.Static); /// @@ -304,6 +305,12 @@ private Expression GetExpressionNullable(string queryValue, PropertyInfo prop, T // try to split up for multiple values var parts = queryValue.Split(','); + if (underlayingType == typeof(DateTime) || underlayingType == typeof(DateTimeOffset)) + { + return GetDateReangeExpression(parts, prop, underlayingType, propertyType, param); + } + + Expression expr = null; foreach (var part in parts) { @@ -328,8 +335,78 @@ private Expression GetExpressionNullable(string queryValue, PropertyInfo prop, T } + private Expression GetDateReangeExpression(string[] parts, PropertyInfo prop, Type underlyingType, Type propertyType, ParameterExpression param) + { + Expression expr = null; + foreach (var part in parts) + { + var mode = ""; + if (!part.Contains("-")) + mode = "year"; + if (part.Contains("-")) + mode = "month"; + if (part.Count(x => x.Equals('-')) == 2) + { + mode = "day"; + if (part.Contains(" ")) // there is a time + { + mode = "hour"; + if (part.Contains(":")) + mode = "minute"; + if (part.Count(x => x.Equals(':')) == 2) + { + mode = "second"; + } + } + } + var partToParse = part; + + // make the datetime valid + if (mode == "year") + partToParse += "-01-01"; + if (mode == "hour") + partToParse += ":00"; + + TypeConverter conv = TypeDescriptor.GetConverter(underlyingType); + dynamic value = conv.ConvertFromInvariantString(partToParse); + var upper =value; + switch (mode) + { + case "year": + upper= upper.AddYears(1); + break; + case "month": + upper = upper.AddMonths(1); + break; + case "day": + upper = upper.AddDays(1); + break; + case "hour": + upper = upper.AddHours(1); + break; + case "minute": + upper = upper.AddMinutes(1); + break; + case "second": + upper = upper.AddSeconds(1); + break; + } + upper = upper.AddTicks(-1); + var methodInfo = GetPropertyExpressionBetweenMethod.MakeGenericMethod(propertyType); + Expression innerExpr = (Expression)methodInfo.Invoke(null, new object[] {value, upper, prop, param}); + if (expr == null) + { + expr = innerExpr; + } + else + { + expr = Expression.OrElse(expr, innerExpr); + } + + } + return expr; + } - private Expression GetExpression(string queryValue, PropertyInfo prop, Type propertyType, ParameterExpression param) { @@ -452,6 +529,17 @@ private static Expression GetPropertyExpression(T value, PropertyInfo propert return Expression.Equal(propertyExpr, castedConstantExpr); } + private static Expression GetPropertyExpressionBetween(T lowerValue, T upperValue, PropertyInfo property, + ParameterExpression param) + { + Expression propertyExpr = Expression.Property(param, property); + var lowerValueExpr = Expression.Constant(lowerValue); + var upperValueExpr = Expression.Constant(upperValue); + Expression lowerCastedConstantExpr = Expression.Convert(lowerValueExpr, typeof(T)); + Expression upperCastedConstantExpr = Expression.Convert(upperValueExpr, typeof(T)); + return Expression.AndAlso(Expression.GreaterThanOrEqual(propertyExpr, lowerCastedConstantExpr), Expression.LessThanOrEqual(propertyExpr, upperCastedConstantExpr)); + } + private static Expression GetEnumPropertyExpression(int? value, PropertyInfo property, ParameterExpression param) { From 9b97852ee126329a4c9cf22710f475a49cc66b82 Mon Sep 17 00:00:00 2001 From: Simon Hofer Date: Thu, 29 Sep 2016 18:22:47 +0200 Subject: [PATCH 4/6] always operate with utc dates --- .../DefaultFilteringTransformerTests.cs | 16 +++++++++++----- .../DefaultFilteringTransformer.cs | 3 +++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs index 6ab52b65..c13def5a 100644 --- a/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs +++ b/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs @@ -156,18 +156,18 @@ public void SetupFixtures() new Dummy { Id = "122", - NullableDateTimeField = new DateTime(1961, 5, 31, 18, 58, 0) + NullableDateTimeField = new DateTime(1961, 5, 31, 18, 58, 0, DateTimeKind.Utc) }, new Dummy { Id = "123", - NullableDateTimeField = new DateTime(1961, 5, 31, 19, 01, 0) + NullableDateTimeField = new DateTime(1961, 5, 31, 19, 01, 0, DateTimeKind.Utc) }, new Dummy { Id = "124", - NullableDateTimeField = new DateTime(1962, 5, 31, 19, 01, 0) + NullableDateTimeField = new DateTime(1962, 5, 31, 19, 01, 0, DateTimeKind.Utc) }, #endregion @@ -870,7 +870,10 @@ public void Filters_by_multiple_matching_nullable_datetime_property_month() [TestMethod] public void Filters_by_multiple_matching_nullable_datetime_property_hour() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-date-time-field]=1961-05-31 19"); + var dt = new DateTime(1961,05,31,19,00,00, DateTimeKind.Utc); + var localdt = dt.ToLocalTime(); + + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-date-time-field]=1961-05-31 " + string.Format("{0,2:D2}", localdt.Hour)); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("123"); } @@ -878,7 +881,10 @@ public void Filters_by_multiple_matching_nullable_datetime_property_hour() [TestMethod] public void Filters_by_multiple_matching_nullable_datetime_property_hour_minute() { - var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-date-time-field]=1961-05-31 19:01"); + var dt = new DateTime(1961, 05, 31, 19, 00, 00, DateTimeKind.Utc); + var localdt = dt.ToLocalTime(); + + var returnedArray = GetArray("http://api.example.com/dummies?filter[nullable-date-time-field]=1961-05-31 " + string.Format("{0,2:D2}", localdt.Hour) + ":01"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("123"); } diff --git a/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs b/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs index 92196a50..a54bc2a8 100644 --- a/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs +++ b/JSONAPI/QueryableTransformers/DefaultFilteringTransformer.cs @@ -392,6 +392,9 @@ private Expression GetDateReangeExpression(string[] parts, PropertyInfo prop, Ty break; } upper = upper.AddTicks(-1); + value = value.ToUniversalTime(); + upper = upper.ToUniversalTime(); + var methodInfo = GetPropertyExpressionBetweenMethod.MakeGenericMethod(propertyType); Expression innerExpr = (Expression)methodInfo.Invoke(null, new object[] {value, upper, prop, param}); if (expr == null) From 906dbc005afac036ff41b82f6a95e3d5f9b616d3 Mon Sep 17 00:00:00 2001 From: Simon Hofer Date: Sat, 1 Oct 2016 09:21:35 +0200 Subject: [PATCH 5/6] added some docs on filtering --- README.md | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/README.md b/README.md index 1fb411ca..68acfa8f 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,85 @@ One of the benefits of the JSON API spec is that it provides several ways to [se > :information_source: As a side note, the RelationAggregator class handles the work of pulling any included related objects into the document--which may be a recursive process: for example, if an object has a relationship of its own type and that relationship property is annotated to be included, then it will recursively include all related objects of the same type! However, those related objects will all be included in a flat array for that type, according to the spec. + +# Filtering + +JSONAPI defines the URL query parameter for filtering is `filter` and should be combined with the associations but there is not much more how the syntax should be. So the JSONAPI.Net framework provides the following filter syntax. + +## Basic +If you want to get a Resource by Id you provide the Id in the URL as part of the path e.g: + +``` URL +/posts/1 +``` +With this call you get the post with Id 1 or a status code 404 if no post with the Id 1 exists. + +If you would filter all related comments on post with the Id 1 you append the name of relationship e.g: + +``` URL +/posts/1/comments +``` + +In both above cases you can add the `filter` query parameter to filter the result on non Id properties. The property to filter on is specified in squared brackets like below. +The value(s) after the equal sing we would call filter value. + +``` URL +/posts/1/comments?filter[autor]=Bob +``` +This will only return objects where "Bob" is the value of the author property. + +``` URL +/posts/1/comments?filter[category]=3 +``` +This will only return objects where 3 is the value of the category property. + +## Multiple value filter + +If you want to filter by multiple values you can concatenate the values separated by comma. In case of strings you need to quote the strings to provide multiple values. + +``` URL +/posts?filter[title]=Post one, which is awesome + => this returns all posts with title "Post one, which is awesome" +``` +``` URL +/posts?filter[title]="Post one","awesome" + => this returns all posts with title "Post one" OR "which is awesome" +``` + +If the field is numeric or DateTime you can concatenate values with comma. +``` URL +/posts?filter[category]=1,2,3 + => this returns all posts with category 1 OR 2 OR 3 +``` +``` URL +/posts?filter[date-created]=2016-09-01,2016-09-02 + => this returns all posts with date-created 2016-09-01 OR 2016-09-02 +``` + +## Wildcard filters + +In case of string properties you can provide a percent sign (%) at the beginning or end of the filter value. This will advice to not compare with equal but with contains. + +``` URL +/posts?filter[title]="%one","%awesome%" + => this returns all posts with title ending on "one" OR containing the word "awesome" +``` + +> :information_source: HINT: the comparison with wildcards is made case **insensitive**. +> :information_source: HINT: If there is a comma inside of the quoted filter value the term gets not split. +> :information_source: HINT: The percent sign is used to start an encoded character in the URL so the filter values **must unconditionally be encoded** before put in an URL. The above example should look like this when sent to server:`filter%5Btitle%5D=%22%25one%22%2C%22%25awesome%25%22` + +## DateTime filters +DateTime filters with equal can be a pain. If you store DateTime with full resolution you must provide the full resolution to make the filter value equal the stored value. + + +## Range filters +There is no implementation on filtering number or date ranges. If you need things like this you can open an issue or even better provide a pull request. + +## Logical operation +There is no implementation on filtering with a tree of logical AND and OR operations. If you need things like this you can open an issue or even better provide a pull request. + + # Great! But what's all this other stuff? From 2d23dd338e36e976b82d70bb0762e87d5e11b655 Mon Sep 17 00:00:00 2001 From: Simon Hofer Date: Sat, 1 Oct 2016 09:32:39 +0200 Subject: [PATCH 6/6] added some docs on filtering --- README.md | 43 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 68acfa8f..5b5e23ca 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ If you want to filter by multiple values you can concatenate the values separate => this returns all posts with title "Post one, which is awesome" ``` ``` URL -/posts?filter[title]="Post one","awesome" +/posts?filter[title]="Post one","which is awesome" => this returns all posts with title "Post one" OR "which is awesome" ``` @@ -119,18 +119,53 @@ In case of string properties you can provide a percent sign (%) at the beginning ``` > :information_source: HINT: the comparison with wildcards is made case **insensitive**. + > :information_source: HINT: If there is a comma inside of the quoted filter value the term gets not split. + > :information_source: HINT: The percent sign is used to start an encoded character in the URL so the filter values **must unconditionally be encoded** before put in an URL. The above example should look like this when sent to server:`filter%5Btitle%5D=%22%25one%22%2C%22%25awesome%25%22` ## DateTime filters -DateTime filters with equal can be a pain. If you store DateTime with full resolution you must provide the full resolution to make the filter value equal the stored value. +DateTime filters with equal can be a pain. If you store DateTime with full resolution (milliseconds) you must provide the full resolution to make the filter value equal the stored value. + +To avoid this problem JSONAPI.Net is automatically filtering by a DateTime range. If you provide a "day" (YYYY-MM-DD) as filter value the filter will be this: `BETWEEN day 00:00:00.000 AND day 23:59:59.999`. + +Now if the property is DateTime or DateTimeOffset you can provide the following types of filter values: +| Part | Format | Filter | +|--------|---------------------|---------------------------------------------------| +| year | YYYY | YYYY-01-01 00:00:00.000 - YYYY-12-31 23:59:59.999 | +| month* | YYYY-MM | YYYY-MM-01 00:00:00.000 - YYYY-MM-31 23:59:59.999 | +| day | YYYY-MM-DD | YYYY-MM-DD 00:00:00.000 - YYYY-MM-DD 23:59:59.999 | +| hour | YYYY-MM-DD HH | YYYY-MM-DD HH:00:00.000 - YYYY-MM-DD HH:59:59.999 | +| minute | YYYY-MM-DD HH:mm | YYYY-MM-DD HH:mm:00.000 - YYYY-MM-DD HH:mm:59.999 | +| second | YYYY-MM-DD HH:mm:ss | YYYY-MM-DD HH:mm:ss.000 - YYYY-MM-DD HH:mm:ss.999 | + +*) assuming this month has 31 days. JSONAPI.Net automatically determines the last day of month by adding one month to the given date. This respects months with less than 31 days. + +If you want to filter all posts created in month May of year 2016 you must provide the format for month filled by your needed date: + + +``` URL +/posts?filter[date-created]=2016-05 + => this returns all posts with date-created in may 2016 +``` ## Range filters -There is no implementation on filtering number or date ranges. If you need things like this you can open an issue or even better provide a pull request. +There is no implementation on filtering number or date ranges. If you need things like that you can open an issue or even better provide a pull request. ## Logical operation -There is no implementation on filtering with a tree of logical AND and OR operations. If you need things like this you can open an issue or even better provide a pull request. +Filters can be combined for multiple fields. You can filter posts created in May 2016 and containing awesome in title: + +``` URL +/posts?filter[date-created]=2016-05&filter[title]="%awesome%" + => this returns all posts with date-created in may 2016 with title containing "awesome" +``` +So we have the following standard behavior to concatenate multiple filters together: +- multiple values on the same property are concatenated with OR +- multiple filters on multiple properties are always concatenated with AND + + +There is no implementation on filtering with a tree of logical AND and OR operations other than described above. If you need things like that you can open an issue or even better provide a pull request. # Great! But what's all this other stuff?