From 5de86dc501b4ca9791ff071ac73484c2a8967893 Mon Sep 17 00:00:00 2001 From: Chris Santero Date: Wed, 21 Jan 2015 11:41:38 -0800 Subject: [PATCH] Create webapi action filter for filtering IQueryables --- .../EnableFilteringAttributeTests.cs | 1089 +++++++++++++++++ JSONAPI.Tests/JSONAPI.Tests.csproj | 1 + .../ActionFilters/EnableFilteringAttribute.cs | 318 +++++ JSONAPI/JSONAPI.csproj | 1 + 4 files changed, 1409 insertions(+) create mode 100644 JSONAPI.Tests/ActionFilters/EnableFilteringAttributeTests.cs create mode 100644 JSONAPI/ActionFilters/EnableFilteringAttribute.cs diff --git a/JSONAPI.Tests/ActionFilters/EnableFilteringAttributeTests.cs b/JSONAPI.Tests/ActionFilters/EnableFilteringAttributeTests.cs new file mode 100644 index 00000000..3c45cd93 --- /dev/null +++ b/JSONAPI.Tests/ActionFilters/EnableFilteringAttributeTests.cs @@ -0,0 +1,1089 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Formatting; +using System.Web.Http.Controllers; +using System.Web.Http.Filters; +using FluentAssertions; +using JSONAPI.ActionFilters; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.Tests.ActionFilters +{ + [TestClass] + public class EnableFilteringAttributeTests + { + private enum SomeEnum + { + EnumValue1 = 1, + EnumValue2 = 2, + EnumValue3 = 3 + } + + private class SomeUnknownType + { + + } + + private class Dummy + { + public string Id { get; set; } + public string StringField { get; set; } + public DateTime DateTimeField { get; set; } + public DateTime? NullableDateTimeField { get; set; } + public DateTimeOffset DateTimeOffsetField { get; set; } + public DateTimeOffset? NullableDateTimeOffsetField { get; set; } + public SomeEnum EnumField { get; set; } + public SomeEnum? NullableEnumField { get; set; } + public Decimal DecimalField { get; set; } + public Decimal? NullableDecimalField { get; set; } + public Boolean BooleanField { get; set; } + public Boolean? NullableBooleanField { get; set; } + public SByte SByteField { get; set; } + public SByte? NullableSByteField { get; set; } + public Byte ByteField { get; set; } + public Byte? NullableByteField { get; set; } + public Int16 Int16Field { get; set; } + public Int16? NullableInt16Field { get; set; } + public UInt16 UInt16Field { get; set; } + public UInt16? NullableUInt16Field { get; set; } + public Int32 Int32Field { get; set; } + public Int32? NullableInt32Field { get; set; } + public UInt32 UInt32Field { get; set; } + public UInt32? NullableUInt32Field { get; set; } + public Int64 Int64Field { get; set; } + public Int64? NullableInt64Field { get; set; } + public UInt64 UInt64Field { get; set; } + public UInt64? NullableUInt64Field { get; set; } + public Double DoubleField { get; set; } + public Double? NullableDoubleField { get; set; } + public Single SingleField { get; set; } + public Single? NullableSingleField { get; set; } + public SomeUnknownType UnknownTypeField { get; set; } + } + + private IQueryable _fixtures; + + [TestInitialize] + public void SetupFixtures() + { + _fixtures = new List + { + #region StringField + + new Dummy + { + Id = "100", + StringField = "String value 1" + }, + new Dummy + { + Id = "101", + StringField = "String value 2" + }, + new Dummy + { + Id = "102", + StringField = "String value 2", + EnumField = SomeEnum.EnumValue3 + }, + + #endregion + + #region DateTimeField + + new Dummy + { + Id = "110", + DateTimeField = new DateTime(1930, 11, 7) + }, + new Dummy + { + Id = "111", + DateTimeField = new DateTime(1980, 6, 2) + }, + + #endregion + + #region NullableDateTimeField + + new Dummy + { + Id = "120", + NullableDateTimeField = new DateTime(1961, 2, 18) + }, + + #endregion + + #region DateTimeOffsetField + + new Dummy + { + Id = "130", + DateTimeOffsetField = new DateTime(1991, 1, 3) + }, + new Dummy + { + Id = "131", + DateTimeOffsetField = new DateTime(2004, 10, 31) + }, + + #endregion + + #region NullableDateTimeOffsetField + + new Dummy + { + Id = "140", + NullableDateTimeOffsetField = new DateTime(2014, 5, 5) + }, + + #endregion + + #region EnumField + + new Dummy + { + Id = "150", + EnumField = SomeEnum.EnumValue1 + }, + new Dummy + { + Id = "151", + EnumField = SomeEnum.EnumValue2 + }, + + #endregion + + #region NullableEnumField + + new Dummy + { + Id = "160", + NullableEnumField = SomeEnum.EnumValue3 + }, + + #endregion + + #region DecimalField + + new Dummy + { + Id = "170", + DecimalField = (decimal) 4.03 + }, + new Dummy + { + Id = "171", + DecimalField = (decimal) 6.37 + }, + + #endregion + + #region NullableDecimalField + + new Dummy + { + Id = "180", + NullableDecimalField = (decimal) 12.09 + }, + + #endregion + + #region BooleanField + + new Dummy + { + Id = "190", + BooleanField = true + }, + new Dummy + { + Id = "191", + BooleanField = false + }, + + #endregion + + #region NullableBooleanField + + new Dummy + { + Id = "200", + NullableBooleanField = false + }, + + #endregion + + #region SByteField + + new Dummy + { + Id = "210", + SByteField = 63 + }, + new Dummy + { + Id = "211", + SByteField = -89 + }, + + #endregion + + #region NullableSByteField + + new Dummy + { + Id = "220", + NullableSByteField = 91 + }, + + #endregion + + #region ByteField + + new Dummy + { + Id = "230", + ByteField = 250 + }, + new Dummy + { + Id = "231", + ByteField = 130 + }, + + #endregion + + #region NullableByteField + + new Dummy + { + Id = "240", + NullableByteField = 44 + }, + + #endregion + + #region Int16Field + + new Dummy + { + Id = "250", + Int16Field = 12345 + }, + new Dummy + { + Id = "251", + Int16Field = -23456 + }, + + #endregion + + #region NullableInt16Field + + new Dummy + { + Id = "260", + NullableInt16Field = 32764 + }, + + #endregion + + #region UInt16Field + + new Dummy + { + Id = "270", + UInt16Field = 12345 + }, + new Dummy + { + Id = "271", + UInt16Field = 45678 + }, + + #endregion + + #region NullableUInt16Field + + new Dummy + { + Id = "280", + NullableUInt16Field = 65000 + }, + + #endregion + + #region Int32Field + + new Dummy + { + Id = "290", + Int32Field = 100000006 + }, + new Dummy + { + Id = "291", + Int32Field = -234567890 + }, + + #endregion + + #region NullableInt32Field + + new Dummy + { + Id = "300", + NullableInt32Field = 345678901 + }, + + #endregion + + #region UInt32Field + + new Dummy + { + Id = "310", + UInt32Field = 123456789 + }, + new Dummy + { + Id = "311", + UInt32Field = 234567890 + }, + + #endregion + + #region NullableUInt32Field + + new Dummy + { + Id = "320", + NullableUInt32Field = 345678901 + }, + + #endregion + + #region Int64Field + + new Dummy + { + Id = "330", + Int64Field = 123453489012 + }, + new Dummy + { + Id = "331", + Int64Field = -234067890123 + }, + + #endregion + + #region NullableInt64Field + + new Dummy + { + Id = "340", + NullableInt64Field = 345671901234 + }, + + #endregion + + #region UInt64Field + + new Dummy + { + Id = "350", + UInt64Field = 123456789012 + }, + new Dummy + { + Id = "351", + UInt64Field = 234567890123 + }, + + #endregion + + #region NullableUInt64Field + + new Dummy + { + Id = "360", + NullableUInt64Field = 345678901234 + }, + + #endregion + + #region SingleField + + new Dummy + { + Id = "370", + SingleField = 21.56901f + }, + new Dummy + { + Id = "371", + SingleField = -34.789f + }, + + #endregion + + #region NullableSingleField + + new Dummy + { + Id = "380", + NullableSingleField = 1.3456f + }, + + #endregion + + #region DoubleField + + new Dummy + { + Id = "390", + DoubleField = 12.3453489012 + }, + new Dummy + { + Id = "391", + DoubleField = -2340678.90123 + }, + + #endregion + + #region NullableDoubleField + + new Dummy + { + Id = "400", + NullableDoubleField = 34567.1901234 + }, + + #endregion + + #region Unknown Type + + new Dummy + { + Id = "1000", + UnknownTypeField = new SomeUnknownType() + } + + #endregion + }.AsQueryable(); + } + + private HttpActionExecutedContext CreateActionExecutedContext(string uri) + { + var formatter = new JsonMediaTypeFormatter(); + + var httpContent = new ObjectContent(typeof(IQueryable), _fixtures, formatter); + + return new HttpActionExecutedContext + { + ActionContext = new HttpActionContext + { + ControllerContext = new HttpControllerContext + { + Request = new HttpRequestMessage(HttpMethod.Get, new Uri(uri)) + } + }, + Response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = httpContent + } + }; + } + + private T[] GetArray(string uri) + { + var filter = new EnableFilteringAttribute(); + + var context = CreateActionExecutedContext(uri); + + filter.OnActionExecuted(context); + + var returnedContent = context.Response.Content as ObjectContent; + returnedContent.Should().NotBeNull(); + returnedContent.ObjectType.Should().Be(typeof(IQueryable)); + + var returnedQueryable = returnedContent.Value as IQueryable; + returnedQueryable.Should().NotBeNull(); + + return returnedQueryable.ToArray(); + } + + #region String + + [TestMethod] + public void Filters_by_matching_string_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?stringField=String value 1"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("100"); + } + + [TestMethod] + public void Filters_by_missing_string_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?stringField="); + returnedArray.Length.Should().Be(46); + } + + #endregion + + #region DateTime + + [TestMethod] + public void Filters_by_matching_datetime_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?dateTimeField=1930-11-07"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("110"); + } + + [TestMethod] + public void Filters_by_missing_datetime_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?dateTimeField="); + returnedArray.Length.Should().Be(0); + } + + [TestMethod] + public void Filters_by_matching_nullable_datetime_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableDateTimeField=1961-02-18"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("120"); + } + + [TestMethod] + public void Filters_by_missing_nullable_datetime_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableDateTimeField="); + returnedArray.Length.Should().Be(48); + returnedArray.Any(d => d.Id == "120").Should().BeFalse(); + } + + #endregion + + #region DateTimeOffset + + [TestMethod] + public void Filters_by_matching_datetimeoffset_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?dateTimeOffsetField=1991-01-03"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("130"); + } + + [TestMethod] + public void Filters_by_missing_datetimeoffset_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?dateTimeOffsetField="); + returnedArray.Length.Should().Be(0); + } + + [TestMethod] + public void Filters_by_matching_nullable_datetimeoffset_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableDateTimeOffsetField=2014-05-05"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("140"); + } + + [TestMethod] + public void Filters_by_missing_nullable_datetimeoffset_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableDateTimeOffsetField="); + returnedArray.Length.Should().Be(48); + returnedArray.Any(d => d.Id == "140").Should().BeFalse(); + } + + #endregion + + #region Enum + + [TestMethod] + public void Filters_by_matching_enum_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?enumField=1"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("150"); + } + + [TestMethod] + public void Filters_by_missing_enum_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?enumField="); + returnedArray.Length.Should().Be(0); + } + + [TestMethod] + public void Filters_by_matching_nullable_enum_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableEnumField=3"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("160"); + } + + [TestMethod] + public void Filters_by_missing_nullable_enum_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableEnumField="); + returnedArray.Length.Should().Be(48); + returnedArray.Any(d => d.Id == "160").Should().BeFalse(); + } + + #endregion + + #region Decimal + + [TestMethod] + public void Filters_by_matching_decimal_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?decimalField=4.03"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("170"); + } + + [TestMethod] + public void Filters_by_missing_decimal_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?decimalField="); + returnedArray.Length.Should().Be(0); + } + + [TestMethod] + public void Filters_by_matching_nullable_decimal_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableDecimalField=12.09"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("180"); + } + + [TestMethod] + public void Filters_by_missing_nullable_decimal_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableDecimalField="); + returnedArray.Length.Should().Be(48); + returnedArray.Any(d => d.Id == "180").Should().BeFalse(); + } + + #endregion + + #region Boolean + + [TestMethod] + public void Filters_by_matching_boolean_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?booleanField=true"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("190"); + } + + [TestMethod] + public void Filters_by_missing_boolean_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?booleanField="); + returnedArray.Length.Should().Be(0); + } + + [TestMethod] + public void Filters_by_matching_nullable_boolean_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableBooleanField=false"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("200"); + } + + [TestMethod] + public void Filters_by_missing_nullable_boolean_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableBooleanField="); + returnedArray.Length.Should().Be(48); + returnedArray.Any(d => d.Id == "200").Should().BeFalse(); + } + + #endregion + + #region SByte + + [TestMethod] + public void Filters_by_matching_sbyte_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?sByteField=63"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("210"); + } + + [TestMethod] + public void Filters_by_missing_sbyte_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?sByteField="); + returnedArray.Length.Should().Be(0); + } + + [TestMethod] + public void Filters_by_matching_nullable_sbyte_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableSByteField=91"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("220"); + } + + [TestMethod] + public void Filters_by_missing_nullable_sbyte_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableSByteField="); + returnedArray.Length.Should().Be(48); + returnedArray.Any(d => d.Id == "220").Should().BeFalse(); + } + + #endregion + + #region Byte + + [TestMethod] + public void Filters_by_matching_byte_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?byteField=250"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("230"); + } + + [TestMethod] + public void Filters_by_missing_byte_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?byteField="); + returnedArray.Length.Should().Be(0); + } + + [TestMethod] + public void Filters_by_matching_nullable_byte_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableByteField=44"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("240"); + } + + [TestMethod] + public void Filters_by_missing_nullable_byte_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableByteField="); + returnedArray.Length.Should().Be(48); + returnedArray.Any(d => d.Id == "240").Should().BeFalse(); + } + + #endregion + + #region Int16 + + [TestMethod] + public void Filters_by_matching_int16_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?int16Field=12345"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("250"); + } + + [TestMethod] + public void Filters_by_missing_int16_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?int16Field="); + returnedArray.Length.Should().Be(0); + } + + [TestMethod] + public void Filters_by_matching_nullable_int16_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableInt16Field=32764"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("260"); + } + + [TestMethod] + public void Filters_by_missing_nullable_int16_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableInt16Field="); + returnedArray.Length.Should().Be(48); + returnedArray.Any(d => d.Id == "260").Should().BeFalse(); + } + + #endregion + + #region UInt16 + + [TestMethod] + public void Filters_by_matching_uint16_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?uInt16Field=12345"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("270"); + } + + [TestMethod] + public void Filters_by_missing_uint16_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?uInt16Field="); + returnedArray.Length.Should().Be(0); + } + + [TestMethod] + public void Filters_by_matching_nullable_uint16_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt16Field=65000"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("280"); + } + + [TestMethod] + public void Filters_by_missing_nullable_uint16_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt16Field="); + returnedArray.Length.Should().Be(48); + returnedArray.Any(d => d.Id == "280").Should().BeFalse(); + } + + #endregion + + #region Int32 + + [TestMethod] + public void Filters_by_matching_int32_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?int32Field=100000006"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("290"); + } + + [TestMethod] + public void Filters_by_missing_int32_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?int32Field="); + returnedArray.Length.Should().Be(0); + } + + [TestMethod] + public void Filters_by_matching_nullable_int32_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableInt32Field=345678901"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("300"); + } + + [TestMethod] + public void Filters_by_missing_nullable_int32_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableInt32Field="); + returnedArray.Length.Should().Be(48); + returnedArray.Any(d => d.Id == "300").Should().BeFalse(); + } + + #endregion + + #region UInt32 + + [TestMethod] + public void Filters_by_matching_uint32_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?uInt32Field=123456789"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("310"); + } + + [TestMethod] + public void Filters_by_missing_uint32_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?uInt32Field="); + returnedArray.Length.Should().Be(0); + } + + [TestMethod] + public void Filters_by_matching_nullable_uint32_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt32Field=345678901"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("320"); + } + + [TestMethod] + public void Filters_by_missing_nullable_uint32_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt32Field="); + returnedArray.Length.Should().Be(48); + returnedArray.Any(d => d.Id == "320").Should().BeFalse(); + } + + #endregion + + #region Int64 + + [TestMethod] + public void Filters_by_matching_int64_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?int64Field=123453489012"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("330"); + } + + [TestMethod] + public void Filters_by_missing_int64_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?int64Field="); + returnedArray.Length.Should().Be(0); + } + + [TestMethod] + public void Filters_by_matching_nullable_int64_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableInt64Field=345671901234"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("340"); + } + + [TestMethod] + public void Filters_by_missing_nullable_int64_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableInt64Field="); + returnedArray.Length.Should().Be(48); + returnedArray.Any(d => d.Id == "340").Should().BeFalse(); + } + + #endregion + + #region UInt64 + + [TestMethod] + public void Filters_by_matching_uint64_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?uInt64Field=123456789012"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("350"); + } + + [TestMethod] + public void Filters_by_missing_uint64_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?uInt64Field="); + returnedArray.Length.Should().Be(0); + } + + [TestMethod] + public void Filters_by_matching_nullable_uint64_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt64Field=345678901234"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("360"); + } + + [TestMethod] + public void Filters_by_missing_nullable_uint64_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt64Field="); + returnedArray.Length.Should().Be(48); + returnedArray.Any(d => d.Id == "360").Should().BeFalse(); + } + + #endregion + + #region Single + + [TestMethod] + public void Filters_by_matching_single_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?singleField=21.56901"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("370"); + } + + [TestMethod] + public void Filters_by_missing_single_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?singleField="); + returnedArray.Length.Should().Be(0); + } + + [TestMethod] + public void Filters_by_matching_nullable_single_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableSingleField=1.3456"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("380"); + } + + [TestMethod] + public void Filters_by_missing_nullable_single_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableSingleField="); + returnedArray.Length.Should().Be(48); + returnedArray.Any(d => d.Id == "380").Should().BeFalse(); + } + + #endregion + + #region Double + + [TestMethod] + public void Filters_by_matching_double_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?doubleField=12.3453489012"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("390"); + } + + [TestMethod] + public void Filters_by_missing_double_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?doubleField="); + returnedArray.Length.Should().Be(0); + } + + [TestMethod] + public void Filters_by_matching_nullable_double_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableDoubleField=34567.1901234"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("400"); + } + + [TestMethod] + public void Filters_by_missing_nullable_double_property() + { + var returnedArray = GetArray("http://api.example.com/dummies?nullableDoubleField="); + returnedArray.Length.Should().Be(48); + returnedArray.Any(d => d.Id == "400").Should().BeFalse(); + } + + #endregion + + #region Unknown Type + + [TestMethod] + public void Does_not_filter_unknown_type() + { + var returnedArray = GetArray("http://api.example.com/dummies?unknownTypeField=asdfasd"); + returnedArray.Length.Should().Be(49); + } + + #endregion + + #region Multiple filters + + [TestMethod] + public void Ands_together_filters() + { + var returnedArray = GetArray("http://api.example.com/dummies?stringField=String value 2&enumField=3"); + returnedArray.Length.Should().Be(1); + returnedArray[0].Id.Should().Be("102"); + } + + #endregion + } +} diff --git a/JSONAPI.Tests/JSONAPI.Tests.csproj b/JSONAPI.Tests/JSONAPI.Tests.csproj index 1aa796f1..9e36c180 100644 --- a/JSONAPI.Tests/JSONAPI.Tests.csproj +++ b/JSONAPI.Tests/JSONAPI.Tests.csproj @@ -75,6 +75,7 @@ + diff --git a/JSONAPI/ActionFilters/EnableFilteringAttribute.cs b/JSONAPI/ActionFilters/EnableFilteringAttribute.cs new file mode 100644 index 00000000..3ebb76e4 --- /dev/null +++ b/JSONAPI/ActionFilters/EnableFilteringAttribute.cs @@ -0,0 +1,318 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Net.Http; +using System.Reflection; +using System.Web.Http.Filters; + +namespace JSONAPI.ActionFilters +{ + public class EnableFilteringAttribute : ActionFilterAttribute + { + // Borrowed from http://stackoverflow.com/questions/3631547/select-right-generic-method-with-reflection + private readonly Lazy _whereMethod = new Lazy(() => + typeof(Queryable).GetMethods() + .Where(x => x.Name == "Where") + .Select(x => new { M = x, P = x.GetParameters() }) + .Where(x => x.P.Length == 2 + && x.P[0].ParameterType.IsGenericType + && x.P[0].ParameterType.GetGenericTypeDefinition() == typeof(IQueryable<>) + && x.P[1].ParameterType.IsGenericType + && x.P[1].ParameterType.GetGenericTypeDefinition() == typeof(Expression<>)) + .Select(x => new { x.M, A = x.P[1].ParameterType.GetGenericArguments() }) + .Where(x => x.A[0].IsGenericType + && x.A[0].GetGenericTypeDefinition() == typeof(Func<,>)) + .Select(x => new { x.M, A = x.A[0].GetGenericArguments() }) + .Where(x => x.A[0].IsGenericParameter + && x.A[1] == typeof(bool)) + .Select(x => x.M) + .SingleOrDefault() + ); + + public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) + { + if (actionExecutedContext.Response != null) + { + var objectContent = actionExecutedContext.Response.Content as ObjectContent; + if (objectContent != null) + { + var objectType = objectContent.ObjectType; + if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(IQueryable<>)) + { + var queryableElementType = objectType.GenericTypeArguments[0]; + var parameter = Expression.Parameter(queryableElementType); + var bodyExpr = GetPredicateBody(actionExecutedContext, parameter); + var lambdaExpr = Expression.Lambda(bodyExpr, parameter); + + var genericMethod = _whereMethod.Value.MakeGenericMethod(queryableElementType); + var filteredQuery = genericMethod.Invoke(null, new[] { objectContent.Value, lambdaExpr }); + + actionExecutedContext.Response.Content = new ObjectContent(objectType, filteredQuery, objectContent.Formatter); + } + } + } + } + + private static Expression GetPredicateBody(HttpActionExecutedContext actionExecutedContext, ParameterExpression param) + { + Expression workingExpr = null; + + var type = param.Type; + var queryPairs = actionExecutedContext.Request.GetQueryNameValuePairs(); + foreach (var queryPair in queryPairs) + { + if (String.IsNullOrWhiteSpace(queryPair.Key)) + continue; + + var prop = type.GetProperty(queryPair.Key) ?? + type.GetProperty(queryPair.Key.Substring(0, 1).ToUpper() + queryPair.Key.Substring(1)); + + if (prop != null) + { + var propertyType = prop.PropertyType; + + var queryValue = queryPair.Value; + if (string.IsNullOrWhiteSpace(queryValue)) + queryValue = null; + + Expression expr; + if (propertyType == typeof (String)) + { + if (String.IsNullOrWhiteSpace(queryValue)) + { + Expression propertyExpr = Expression.Property(param, prop); + expr = Expression.Equal(propertyExpr, Expression.Constant(null)); + } + 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, out value) + ? GetPropertyExpression(value, prop, param) + : Expression.Constant(false); + } + else if (propertyType == typeof(Single?)) + { + Single tmp; + var value = Single.TryParse(queryValue, out tmp) ? tmp : (Single?)null; + expr = GetPropertyExpression(value, prop, param); + } + else if (propertyType == typeof(Double)) + { + Double value; + expr = Double.TryParse(queryValue, out value) + ? GetPropertyExpression(value, prop, param) + : Expression.Constant(false); + } + else if (propertyType == typeof(Double?)) + { + Double tmp; + var value = Double.TryParse(queryValue, out tmp) ? tmp : (Double?)null; + expr = GetPropertyExpression(value, prop, param); + } + else if (propertyType == typeof(Decimal)) + { + Decimal value; + expr = Decimal.TryParse(queryValue, out value) + ? GetPropertyExpression(value, prop, param) + : Expression.Constant(false); + } + else if (propertyType == typeof(Decimal?)) + { + Decimal tmp; + var value = Decimal.TryParse(queryValue, out tmp) ? tmp : (Decimal?)null; + expr = GetPropertyExpression(value, prop, param); + } + else if (propertyType == typeof(DateTime)) + { + DateTime value; + expr = DateTime.TryParse(queryValue, out value) + ? GetPropertyExpression(value, prop, param) + : Expression.Constant(false); + } + else if (propertyType == typeof(DateTime?)) + { + DateTime tmp; + var value = DateTime.TryParse(queryValue, out tmp) ? tmp : (DateTime?)null; + expr = GetPropertyExpression(value, prop, param); + } + else if (propertyType == typeof(DateTimeOffset)) + { + DateTimeOffset value; + expr = DateTimeOffset.TryParse(queryValue, out value) + ? GetPropertyExpression(value, prop, param) + : Expression.Constant(false); + } + else if (propertyType == typeof (DateTimeOffset?)) + { + DateTimeOffset tmp; + var value = DateTimeOffset.TryParse(queryValue, out tmp) ? tmp : (DateTimeOffset?)null; + expr = GetPropertyExpression(value, prop, param); + } + else if (propertyType.IsEnum) + { + int value; + expr = (int.TryParse(queryValue, out value) && Enum.IsDefined(propertyType, value)) + ? GetEnumPropertyExpression(value, prop, param) + : Expression.Constant(false); + } + else if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof (Nullable<>) && + propertyType.GenericTypeArguments[0].IsEnum) + { + int tmp; + var value = int.TryParse(queryValue, out tmp) ? tmp : (int?)null; + expr = GetEnumPropertyExpression(value, prop, param); + } + else + { + expr = Expression.Constant(true); + } + + workingExpr = workingExpr == null ? expr : Expression.AndAlso(workingExpr, expr); + } + } + + return workingExpr ?? Expression.Constant(true); // No filters, so return everything + } + + private static Expression GetPropertyExpression(T value, PropertyInfo property, + ParameterExpression param) + { + Expression propertyExpr = Expression.Property(param, property); + var valueExpr = Expression.Constant(value); + Expression castedConstantExpr = Expression.Convert(valueExpr, typeof(T)); + return Expression.Equal(propertyExpr, castedConstantExpr); + } + + private static Expression GetEnumPropertyExpression(int? value, PropertyInfo property, + ParameterExpression param) + { + Expression propertyExpr = Expression.Property(param, property); + var castedValueExpr = Expression.Convert(Expression.Constant(value), typeof(int?)); + var castedPropertyExpr = Expression.Convert(propertyExpr, typeof(int?)); + return Expression.Equal(castedPropertyExpr, castedValueExpr); + } + } +} \ No newline at end of file diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj index d3734706..b7f4ff2b 100644 --- a/JSONAPI/JSONAPI.csproj +++ b/JSONAPI/JSONAPI.csproj @@ -64,6 +64,7 @@ +