diff --git a/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs b/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs index 8487230c..a26cc087 100644 --- a/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs +++ b/JSONAPI.EntityFramework.Tests.TestWebApp/Startup.cs @@ -1,17 +1,11 @@ using System; -using System.Collections.Generic; using System.Reflection; using System.Web; using System.Web.Http; -using System.Web.Http.Dispatcher; using Autofac; using Autofac.Integration.WebApi; -using JSONAPI.ActionFilters; using JSONAPI.Core; -using JSONAPI.EntityFramework.ActionFilters; using JSONAPI.EntityFramework.Tests.TestWebApp.Models; -using JSONAPI.Http; -using JSONAPI.Json; using Microsoft.Owin; using Owin; @@ -62,32 +56,25 @@ public void Configuration(IAppBuilder app) private static HttpConfiguration GetWebApiConfiguration() { - var config = new HttpConfiguration(); - var pluralizationService = new PluralizationService(); - var modelManager = new ModelManager(pluralizationService); - modelManager.RegisterResourceType(typeof(Comment)); - modelManager.RegisterResourceType(typeof(Post)); - modelManager.RegisterResourceType(typeof(Tag)); - modelManager.RegisterResourceType(typeof(User)); - modelManager.RegisterResourceType(typeof(UserGroup)); - - var formatter = new JsonApiFormatter(modelManager); - config.Formatters.Clear(); - config.Formatters.Add(formatter); + var httpConfig = new HttpConfiguration(); - // Global filters - config.Filters.Add(new EnumerateQueryableAsyncAttribute()); - config.Filters.Add(new EnableSortingAttribute(modelManager)); - config.Filters.Add(new EnableFilteringAttribute(modelManager)); + // Configure JSON API + new JsonApiConfiguration() + .PluralizeResourceTypesWith(pluralizationService) + .UseEntityFramework() + .RegisterResourceType(typeof(Comment)) + .RegisterResourceType(typeof(Post)) + .RegisterResourceType(typeof(Tag)) + .RegisterResourceType(typeof(User)) + .RegisterResourceType(typeof(UserGroup)) + .Apply(httpConfig); - // Override controller selector - config.Services.Replace(typeof(IHttpControllerSelector), new PascalizedControllerSelector(config)); // Web API routes - config.Routes.MapHttpRoute("DefaultApi", "{controller}/{id}", new { id = RouteParameter.Optional }); + httpConfig.Routes.MapHttpRoute("DefaultApi", "{controller}/{id}", new { id = RouteParameter.Optional }); - return config; + return httpConfig; } } } \ No newline at end of file diff --git a/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs b/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs index d1be68eb..1b590311 100644 --- a/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs +++ b/JSONAPI.EntityFramework.Tests/Acceptance/AcceptanceTestsBase.cs @@ -1,6 +1,5 @@ using System; using System.Data.Common; -using System.Linq; using System.Net; using System.Net.Http; using System.Text; diff --git a/JSONAPI.EntityFramework.Tests/ActionFilters/EnumerateQueryableAsyncAttributeTests.cs b/JSONAPI.EntityFramework.Tests/ActionFilters/AsynchronousEnumerationTransformerTests.cs similarity index 61% rename from JSONAPI.EntityFramework.Tests/ActionFilters/EnumerateQueryableAsyncAttributeTests.cs rename to JSONAPI.EntityFramework.Tests/ActionFilters/AsynchronousEnumerationTransformerTests.cs index 42f86fd1..609c95ed 100644 --- a/JSONAPI.EntityFramework.Tests/ActionFilters/EnumerateQueryableAsyncAttributeTests.cs +++ b/JSONAPI.EntityFramework.Tests/ActionFilters/AsynchronousEnumerationTransformerTests.cs @@ -6,7 +6,6 @@ using System.Net; using System.Net.Http; using System.Net.Http.Formatting; -using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web.Http.Controllers; @@ -20,7 +19,7 @@ namespace JSONAPI.EntityFramework.Tests.ActionFilters { [TestClass] - public class EnumerateQueryableAsyncAttributeTests + public class AsynchronousEnumerationTransformerTests { public class Dummy { @@ -54,7 +53,7 @@ public void SetupFixtures() }.AsQueryable(); } - private HttpActionExecutedContext CreateActionExecutedContext(IDbAsyncEnumerator asyncEnumerator) + private IQueryable CreateQueryable(IDbAsyncEnumerator asyncEnumerator) { var mockSet = new Mock>(); mockSet.As>() @@ -69,39 +68,17 @@ private HttpActionExecutedContext CreateActionExecutedContext(IDbAsyncEnumerator mockSet.As>().Setup(m => m.ElementType).Returns(_fixtures.ElementType); mockSet.As>().Setup(m => m.GetEnumerator()).Returns(_fixtures.GetEnumerator()); - var formatter = new JsonMediaTypeFormatter(); - - var httpContent = new ObjectContent(typeof(IQueryable), mockSet.Object, formatter); - - return new HttpActionExecutedContext - { - ActionContext = new HttpActionContext - { - ControllerContext = new HttpControllerContext - { - Request = new HttpRequestMessage(HttpMethod.Get, "http://api.example.com/dummies") - } - }, - Response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = httpContent - } - }; + return mockSet.Object; } [TestMethod] public async Task ResolvesQueryable() { - var actionFilter = new EnumerateQueryableAsyncAttribute(); - - var context = CreateActionExecutedContext(new TestDbAsyncEnumerator(_fixtures.GetEnumerator())); - - await actionFilter.OnActionExecutedAsync(context, new CancellationToken()); + var transformer = new AsynchronousEnumerationTransformer(); - var objectContent = context.Response.Content as ObjectContent; - objectContent.Should().NotBeNull(); + var query = CreateQueryable(new TestDbAsyncEnumerator(_fixtures.GetEnumerator())); - var array = objectContent.Value as Dummy[]; + var array = await transformer.Enumerate(query, new CancellationToken()); array.Should().NotBeNull(); array.Length.Should().Be(3); array[0].Id.Should().Be("1"); @@ -112,16 +89,16 @@ public async Task ResolvesQueryable() [TestMethod] public void CancelsProperly() { - var actionFilter = new EnumerateQueryableAsyncAttribute(); + var actionFilter = new AsynchronousEnumerationTransformer(); - var context = CreateActionExecutedContext(new WaitsUntilCancellationDbAsyncEnumerator(1000, _fixtures.GetEnumerator())); + var context = CreateQueryable(new WaitsUntilCancellationDbAsyncEnumerator(1000, _fixtures.GetEnumerator())); var cts = new CancellationTokenSource(); cts.CancelAfter(300); Func action = async () => { - await actionFilter.OnActionExecutedAsync(context, cts.Token); + await actionFilter.Enumerate(context, cts.Token); }; action.ShouldThrow(); } diff --git a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj index 0f544ccd..214450ca 100644 --- a/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj +++ b/JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj @@ -115,7 +115,7 @@ - + diff --git a/JSONAPI.EntityFramework/ActionFilters/AsynchronousEnumerationTransformer.cs b/JSONAPI.EntityFramework/ActionFilters/AsynchronousEnumerationTransformer.cs new file mode 100644 index 00000000..55a74832 --- /dev/null +++ b/JSONAPI.EntityFramework/ActionFilters/AsynchronousEnumerationTransformer.cs @@ -0,0 +1,30 @@ +using System; +using System.Data.Entity; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using JSONAPI.ActionFilters; + +namespace JSONAPI.EntityFramework.ActionFilters +{ + /// + /// Enumerates an IQueryable asynchronously using Entity Framework's ToArrayAsync() method. + /// + public class AsynchronousEnumerationTransformer : IQueryableEnumerationTransformer + { + private readonly Lazy _toArrayAsyncMethod = new Lazy(() => + typeof(QueryableExtensions).GetMethods().FirstOrDefault(x => x.Name == "ToArrayAsync" && x.GetParameters().Count() == 2)); + + public async Task Enumerate(IQueryable query, CancellationToken cancellationToken) + { + var queryableElementType = typeof (T); + var openToArrayAsyncMethod = _toArrayAsyncMethod.Value; + var toArrayAsyncMethod = openToArrayAsyncMethod.MakeGenericMethod(queryableElementType); + var invocation = (dynamic)toArrayAsyncMethod.Invoke(null, new object[] { query, cancellationToken }); + + var resultArray = await invocation; + return resultArray; + } + } +} diff --git a/JSONAPI.EntityFramework/ActionFilters/EnumerateQueryableAsyncAttribute.cs b/JSONAPI.EntityFramework/ActionFilters/EnumerateQueryableAsyncAttribute.cs deleted file mode 100644 index 40e0f47b..00000000 --- a/JSONAPI.EntityFramework/ActionFilters/EnumerateQueryableAsyncAttribute.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Data.Entity; -using System.Linq; -using System.Net.Http; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using System.Web.Http.Filters; - -namespace JSONAPI.EntityFramework.ActionFilters -{ - public class EnumerateQueryableAsyncAttribute : ActionFilterAttribute - { - private readonly Lazy _toArrayAsyncMethod = new Lazy(() => - typeof(QueryableExtensions).GetMethods().FirstOrDefault(x => x.Name == "ToArrayAsync" && x.GetParameters().Count() == 2)); - - public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken) - { - 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 openToArrayAsyncMethod = _toArrayAsyncMethod.Value; - var toArrayAsyncMethod = openToArrayAsyncMethod.MakeGenericMethod(queryableElementType); - var invocation = (dynamic)toArrayAsyncMethod.Invoke(null, new[] { objectContent.Value, cancellationToken }); - - var resultArray = await invocation; - actionExecutedContext.Response.Content = new ObjectContent(resultArray.GetType(), resultArray, objectContent.Formatter); - } - } - } - } - } -} diff --git a/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj b/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj index 5953bb90..f92e7dce 100644 --- a/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj +++ b/JSONAPI.EntityFramework/JSONAPI.EntityFramework.csproj @@ -70,10 +70,11 @@ - + + diff --git a/JSONAPI.EntityFramework/JsonApiConfigurationExtensions.cs b/JSONAPI.EntityFramework/JsonApiConfigurationExtensions.cs new file mode 100644 index 00000000..08906ba6 --- /dev/null +++ b/JSONAPI.EntityFramework/JsonApiConfigurationExtensions.cs @@ -0,0 +1,23 @@ +using JSONAPI.Core; +using JSONAPI.EntityFramework.ActionFilters; + +namespace JSONAPI.EntityFramework +{ + /// + /// Extension Methods for JSONAPI.JsonApiConfiguration + /// + public static class JsonApiConfigurationExtensions + { + /// + /// Add Entity Framework specific handling to the configuration + /// + /// The configuration object to modify + /// The same configuration object that was passed in + public static JsonApiConfiguration UseEntityFramework(this JsonApiConfiguration jsonApiConfig) + { + jsonApiConfig.EnumerateQueriesWith(new AsynchronousEnumerationTransformer()); + + return jsonApiConfig; + } + } +} diff --git a/JSONAPI.Tests/ActionFilters/EnableFilteringAttributeTests.cs b/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs similarity index 76% rename from JSONAPI.Tests/ActionFilters/EnableFilteringAttributeTests.cs rename to JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs index 61223884..32154ad7 100644 --- a/JSONAPI.Tests/ActionFilters/EnableFilteringAttributeTests.cs +++ b/JSONAPI.Tests/ActionFilters/DefaultFilteringTransformerTests.cs @@ -3,20 +3,16 @@ using System.Linq; using System.Net; using System.Net.Http; -using System.Net.Http.Formatting; using System.Web.Http; -using System.Web.Http.Controllers; -using System.Web.Http.Filters; using FluentAssertions; using JSONAPI.ActionFilters; using JSONAPI.Core; -using JSONAPI.Json; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace JSONAPI.Tests.ActionFilters { [TestClass] - public class EnableFilteringAttributeTests + public class DefaultFilteringTransformerTests : QueryableTransformerTestsBase { private enum SomeEnum { @@ -539,48 +535,21 @@ public void SetupFixtures() _fixturesQuery = _fixtures.AsQueryable(); } - private HttpActionExecutedContext CreateActionExecutedContext(IModelManager modelManager, string uri) + private DefaultFilteringTransformer GetTransformer() { - var formatter = new JsonApiFormatter(modelManager); - - var httpContent = new ObjectContent(typeof(IQueryable), _fixturesQuery, formatter); - - return new HttpActionExecutedContext + var pluralizationService = new PluralizationService(new Dictionary { - 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 modelManager = new ModelManager(new PluralizationService()); + {"Dummy", "Dummies"} + }); + var modelManager = new ModelManager(pluralizationService); modelManager.RegisterResourceType(typeof(Dummy)); modelManager.RegisterResourceType(typeof(RelatedItemWithId)); + return new DefaultFilteringTransformer(modelManager); + } - var filter = new EnableFilteringAttribute(modelManager); - - var context = CreateActionExecutedContext(modelManager, 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(); + private Dummy[] GetArray(string uri) + { + return Transform(GetTransformer(), _fixturesQuery, uri).ToArray(); } #region String @@ -588,7 +557,7 @@ private T[] GetArray(string uri) [TestMethod] public void Filters_by_matching_string_property() { - var returnedArray = GetArray("http://api.example.com/dummies?stringField=String value 1"); + var returnedArray = GetArray("http://api.example.com/dummies?stringField=String value 1"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("100"); } @@ -596,7 +565,7 @@ public void Filters_by_matching_string_property() [TestMethod] public void Filters_by_missing_string_property() { - var returnedArray = GetArray("http://api.example.com/dummies?stringField="); + var returnedArray = GetArray("http://api.example.com/dummies?stringField="); returnedArray.Length.Should().Be(_fixtures.Count - 3); returnedArray.Any(d => d.Id == "100" || d.Id == "101" || d.Id == "102").Should().BeFalse(); } @@ -608,7 +577,7 @@ public void Filters_by_missing_string_property() [TestMethod] public void Filters_by_matching_datetime_property() { - var returnedArray = GetArray("http://api.example.com/dummies?dateTimeField=1930-11-07"); + var returnedArray = GetArray("http://api.example.com/dummies?dateTimeField=1930-11-07"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("110"); } @@ -616,14 +585,14 @@ public void Filters_by_matching_datetime_property() [TestMethod] public void Filters_by_missing_datetime_property() { - var returnedArray = GetArray("http://api.example.com/dummies?dateTimeField="); + 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"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableDateTimeField=1961-02-18"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("120"); } @@ -631,7 +600,7 @@ public void Filters_by_matching_nullable_datetime_property() [TestMethod] public void Filters_by_missing_nullable_datetime_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableDateTimeField="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableDateTimeField="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "120").Should().BeFalse(); } @@ -643,7 +612,7 @@ public void Filters_by_missing_nullable_datetime_property() [TestMethod] public void Filters_by_matching_datetimeoffset_property() { - var returnedArray = GetArray("http://api.example.com/dummies?dateTimeOffsetField=1991-01-03"); + var returnedArray = GetArray("http://api.example.com/dummies?dateTimeOffsetField=1991-01-03"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("130"); } @@ -651,14 +620,14 @@ public void Filters_by_matching_datetimeoffset_property() [TestMethod] public void Filters_by_missing_datetimeoffset_property() { - var returnedArray = GetArray("http://api.example.com/dummies?dateTimeOffsetField="); + 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"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableDateTimeOffsetField=2014-05-05"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("140"); } @@ -666,7 +635,7 @@ public void Filters_by_matching_nullable_datetimeoffset_property() [TestMethod] public void Filters_by_missing_nullable_datetimeoffset_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableDateTimeOffsetField="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableDateTimeOffsetField="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "140").Should().BeFalse(); } @@ -678,7 +647,7 @@ public void Filters_by_missing_nullable_datetimeoffset_property() [TestMethod] public void Filters_by_matching_enum_property() { - var returnedArray = GetArray("http://api.example.com/dummies?enumField=1"); + var returnedArray = GetArray("http://api.example.com/dummies?enumField=1"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("150"); } @@ -686,14 +655,14 @@ public void Filters_by_matching_enum_property() [TestMethod] public void Filters_by_missing_enum_property() { - var returnedArray = GetArray("http://api.example.com/dummies?enumField="); + 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"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableEnumField=3"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("160"); } @@ -701,7 +670,7 @@ public void Filters_by_matching_nullable_enum_property() [TestMethod] public void Filters_by_missing_nullable_enum_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableEnumField="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableEnumField="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "160").Should().BeFalse(); } @@ -713,7 +682,7 @@ public void Filters_by_missing_nullable_enum_property() [TestMethod] public void Filters_by_matching_decimal_property() { - var returnedArray = GetArray("http://api.example.com/dummies?decimalField=4.03"); + var returnedArray = GetArray("http://api.example.com/dummies?decimalField=4.03"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("170"); } @@ -721,14 +690,14 @@ public void Filters_by_matching_decimal_property() [TestMethod] public void Filters_by_missing_decimal_property() { - var returnedArray = GetArray("http://api.example.com/dummies?decimalField="); + 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"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableDecimalField=12.09"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("180"); } @@ -736,7 +705,7 @@ public void Filters_by_matching_nullable_decimal_property() [TestMethod] public void Filters_by_missing_nullable_decimal_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableDecimalField="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableDecimalField="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "180").Should().BeFalse(); } @@ -748,7 +717,7 @@ public void Filters_by_missing_nullable_decimal_property() [TestMethod] public void Filters_by_matching_boolean_property() { - var returnedArray = GetArray("http://api.example.com/dummies?booleanField=true"); + var returnedArray = GetArray("http://api.example.com/dummies?booleanField=true"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("190"); } @@ -756,14 +725,14 @@ public void Filters_by_matching_boolean_property() [TestMethod] public void Filters_by_missing_boolean_property() { - var returnedArray = GetArray("http://api.example.com/dummies?booleanField="); + 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"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableBooleanField=false"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("200"); } @@ -771,7 +740,7 @@ public void Filters_by_matching_nullable_boolean_property() [TestMethod] public void Filters_by_missing_nullable_boolean_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableBooleanField="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableBooleanField="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "200").Should().BeFalse(); } @@ -783,7 +752,7 @@ public void Filters_by_missing_nullable_boolean_property() [TestMethod] public void Filters_by_matching_sbyte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?sByteField=63"); + var returnedArray = GetArray("http://api.example.com/dummies?sByteField=63"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("210"); } @@ -791,14 +760,14 @@ public void Filters_by_matching_sbyte_property() [TestMethod] public void Filters_by_missing_sbyte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?sByteField="); + 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"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableSByteField=91"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("220"); } @@ -806,7 +775,7 @@ public void Filters_by_matching_nullable_sbyte_property() [TestMethod] public void Filters_by_missing_nullable_sbyte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableSByteField="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableSByteField="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "220").Should().BeFalse(); } @@ -818,7 +787,7 @@ public void Filters_by_missing_nullable_sbyte_property() [TestMethod] public void Filters_by_matching_byte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?byteField=250"); + var returnedArray = GetArray("http://api.example.com/dummies?byteField=250"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("230"); } @@ -826,14 +795,14 @@ public void Filters_by_matching_byte_property() [TestMethod] public void Filters_by_missing_byte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?byteField="); + 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"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableByteField=44"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("240"); } @@ -841,7 +810,7 @@ public void Filters_by_matching_nullable_byte_property() [TestMethod] public void Filters_by_missing_nullable_byte_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableByteField="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableByteField="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "240").Should().BeFalse(); } @@ -853,7 +822,7 @@ public void Filters_by_missing_nullable_byte_property() [TestMethod] public void Filters_by_matching_int16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?int16Field=12345"); + var returnedArray = GetArray("http://api.example.com/dummies?int16Field=12345"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("250"); } @@ -861,14 +830,14 @@ public void Filters_by_matching_int16_property() [TestMethod] public void Filters_by_missing_int16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?int16Field="); + 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"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableInt16Field=32764"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("260"); } @@ -876,7 +845,7 @@ public void Filters_by_matching_nullable_int16_property() [TestMethod] public void Filters_by_missing_nullable_int16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableInt16Field="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableInt16Field="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "260").Should().BeFalse(); } @@ -888,7 +857,7 @@ public void Filters_by_missing_nullable_int16_property() [TestMethod] public void Filters_by_matching_uint16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?uInt16Field=12345"); + var returnedArray = GetArray("http://api.example.com/dummies?uInt16Field=12345"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("270"); } @@ -896,14 +865,14 @@ public void Filters_by_matching_uint16_property() [TestMethod] public void Filters_by_missing_uint16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?uInt16Field="); + 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"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt16Field=65000"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("280"); } @@ -911,7 +880,7 @@ public void Filters_by_matching_nullable_uint16_property() [TestMethod] public void Filters_by_missing_nullable_uint16_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt16Field="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt16Field="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "280").Should().BeFalse(); } @@ -923,7 +892,7 @@ public void Filters_by_missing_nullable_uint16_property() [TestMethod] public void Filters_by_matching_int32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?int32Field=100000006"); + var returnedArray = GetArray("http://api.example.com/dummies?int32Field=100000006"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("290"); } @@ -931,14 +900,14 @@ public void Filters_by_matching_int32_property() [TestMethod] public void Filters_by_missing_int32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?int32Field="); + 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"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableInt32Field=345678901"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("300"); } @@ -946,7 +915,7 @@ public void Filters_by_matching_nullable_int32_property() [TestMethod] public void Filters_by_missing_nullable_int32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableInt32Field="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableInt32Field="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "300").Should().BeFalse(); } @@ -958,7 +927,7 @@ public void Filters_by_missing_nullable_int32_property() [TestMethod] public void Filters_by_matching_uint32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?uInt32Field=123456789"); + var returnedArray = GetArray("http://api.example.com/dummies?uInt32Field=123456789"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("310"); } @@ -966,14 +935,14 @@ public void Filters_by_matching_uint32_property() [TestMethod] public void Filters_by_missing_uint32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?uInt32Field="); + 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"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt32Field=345678901"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("320"); } @@ -981,7 +950,7 @@ public void Filters_by_matching_nullable_uint32_property() [TestMethod] public void Filters_by_missing_nullable_uint32_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt32Field="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt32Field="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "320").Should().BeFalse(); } @@ -993,7 +962,7 @@ public void Filters_by_missing_nullable_uint32_property() [TestMethod] public void Filters_by_matching_int64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?int64Field=123453489012"); + var returnedArray = GetArray("http://api.example.com/dummies?int64Field=123453489012"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("330"); } @@ -1001,14 +970,14 @@ public void Filters_by_matching_int64_property() [TestMethod] public void Filters_by_missing_int64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?int64Field="); + 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"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableInt64Field=345671901234"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("340"); } @@ -1016,7 +985,7 @@ public void Filters_by_matching_nullable_int64_property() [TestMethod] public void Filters_by_missing_nullable_int64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableInt64Field="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableInt64Field="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "340").Should().BeFalse(); } @@ -1028,7 +997,7 @@ public void Filters_by_missing_nullable_int64_property() [TestMethod] public void Filters_by_matching_uint64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?uInt64Field=123456789012"); + var returnedArray = GetArray("http://api.example.com/dummies?uInt64Field=123456789012"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("350"); } @@ -1036,14 +1005,14 @@ public void Filters_by_matching_uint64_property() [TestMethod] public void Filters_by_missing_uint64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?uInt64Field="); + 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"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt64Field=345678901234"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("360"); } @@ -1051,7 +1020,7 @@ public void Filters_by_matching_nullable_uint64_property() [TestMethod] public void Filters_by_missing_nullable_uint64_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt64Field="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableUInt64Field="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "360").Should().BeFalse(); } @@ -1063,7 +1032,7 @@ public void Filters_by_missing_nullable_uint64_property() [TestMethod] public void Filters_by_matching_single_property() { - var returnedArray = GetArray("http://api.example.com/dummies?singleField=21.56901"); + var returnedArray = GetArray("http://api.example.com/dummies?singleField=21.56901"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("370"); } @@ -1071,14 +1040,14 @@ public void Filters_by_matching_single_property() [TestMethod] public void Filters_by_missing_single_property() { - var returnedArray = GetArray("http://api.example.com/dummies?singleField="); + 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"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableSingleField=1.3456"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("380"); } @@ -1086,7 +1055,7 @@ public void Filters_by_matching_nullable_single_property() [TestMethod] public void Filters_by_missing_nullable_single_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableSingleField="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableSingleField="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "380").Should().BeFalse(); } @@ -1098,7 +1067,7 @@ public void Filters_by_missing_nullable_single_property() [TestMethod] public void Filters_by_matching_double_property() { - var returnedArray = GetArray("http://api.example.com/dummies?doubleField=12.3453489012"); + var returnedArray = GetArray("http://api.example.com/dummies?doubleField=12.3453489012"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("390"); } @@ -1106,14 +1075,14 @@ public void Filters_by_matching_double_property() [TestMethod] public void Filters_by_missing_double_property() { - var returnedArray = GetArray("http://api.example.com/dummies?doubleField="); + 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"); + var returnedArray = GetArray("http://api.example.com/dummies?nullableDoubleField=34567.1901234"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("400"); } @@ -1121,7 +1090,7 @@ public void Filters_by_matching_nullable_double_property() [TestMethod] public void Filters_by_missing_nullable_double_property() { - var returnedArray = GetArray("http://api.example.com/dummies?nullableDoubleField="); + var returnedArray = GetArray("http://api.example.com/dummies?nullableDoubleField="); returnedArray.Length.Should().Be(_fixtures.Count - 1); returnedArray.Any(d => d.Id == "400").Should().BeFalse(); } @@ -1133,7 +1102,7 @@ public void Filters_by_missing_nullable_double_property() [TestMethod] public void Does_not_filter_unknown_type() { - Action action = () => GetArray("http://api.example.com/dummies?unknownTypeField=asdfasd"); + Action action = () => GetArray("http://api.example.com/dummies?unknownTypeField=asdfasd"); action.ShouldThrow().Which.Response.StatusCode.Should().Be(HttpStatusCode.BadRequest); } @@ -1144,7 +1113,7 @@ public void Does_not_filter_unknown_type() [TestMethod] public void Filters_by_matching_to_one_relationship_id() { - var returnedArray = GetArray("http://api.example.com/dummies?toOneRelatedItem=1101"); + var returnedArray = GetArray("http://api.example.com/dummies?toOneRelatedItem=1101"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("1100"); } @@ -1152,7 +1121,7 @@ public void Filters_by_matching_to_one_relationship_id() [TestMethod] public void Filters_by_missing_to_one_relationship_id() { - var returnedArray = GetArray("http://api.example.com/dummies?toOneRelatedItem="); + var returnedArray = GetArray("http://api.example.com/dummies?toOneRelatedItem="); returnedArray.Length.Should().Be(_fixtures.Count - 2); returnedArray.Any(d => d.Id == "1100" || d.Id == "1102").Should().BeFalse(); } @@ -1164,7 +1133,7 @@ public void Filters_by_missing_to_one_relationship_id() [TestMethod] public void Filters_by_matching_id_in_to_many_relationship() { - var returnedArray = GetArray("http://api.example.com/dummies?toManyRelatedItems=1111"); + var returnedArray = GetArray("http://api.example.com/dummies?toManyRelatedItems=1111"); returnedArray.Length.Should().Be(1); returnedArray[0].Id.Should().Be("1110"); } @@ -1172,7 +1141,7 @@ public void Filters_by_matching_id_in_to_many_relationship() [TestMethod] public void Filters_by_missing_id_in_to_many_relationship() { - var returnedArray = GetArray("http://api.example.com/dummies?toManyRelatedItems="); + var returnedArray = GetArray("http://api.example.com/dummies?toManyRelatedItems="); returnedArray.Length.Should().Be(_fixtures.Count - 2); returnedArray.Any(d => d.Id == "1110" || d.Id == "1120").Should().BeFalse(); } @@ -1184,7 +1153,7 @@ public void Filters_by_missing_id_in_to_many_relationship() [TestMethod] public void Ands_together_filters() { - var returnedArray = GetArray("http://api.example.com/dummies?stringField=String value 2&enumField=3"); + 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"); } diff --git a/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs b/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs new file mode 100644 index 00000000..dbcbb4fa --- /dev/null +++ b/JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using FluentAssertions; +using JSONAPI.ActionFilters; +using JSONAPI.Core; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace JSONAPI.Tests.ActionFilters +{ + [TestClass] + public class DefaultSortingTransformerTests : QueryableTransformerTestsBase + { + private class Dummy + { + // ReSharper disable UnusedAutoPropertyAccessor.Local + public string Id { get; set; } + // ReSharper restore UnusedAutoPropertyAccessor.Local + + public string FirstName { get; set; } + + public string LastName { get; set; } + } + + private IList _fixtures; + private IQueryable _fixturesQuery; + + [TestInitialize] + public void SetupFixtures() + { + _fixtures = new List + { + new Dummy { Id = "1", FirstName = "Thomas", LastName = "Paine" }, + new Dummy { Id = "2", FirstName = "Samuel", LastName = "Adams" }, + new Dummy { Id = "3", FirstName = "George", LastName = "Washington"}, + new Dummy { Id = "4", FirstName = "Thomas", LastName = "Jefferson" }, + new Dummy { Id = "5", FirstName = "Martha", LastName = "Washington"}, + new Dummy { Id = "6", FirstName = "Abraham", LastName = "Lincoln" }, + new Dummy { Id = "7", FirstName = "Andrew", LastName = "Jackson" }, + new Dummy { Id = "8", FirstName = "Andrew", LastName = "Johnson" }, + new Dummy { Id = "8", FirstName = "William", LastName = "Harrison" } + }; + _fixturesQuery = _fixtures.AsQueryable(); + } + + private DefaultSortingTransformer GetTransformer() + { + var pluralizationService = new PluralizationService(new Dictionary + { + {"Dummy", "Dummies"} + }); + var modelManager = new ModelManager(pluralizationService); + modelManager.RegisterResourceType(typeof(Dummy)); + return new DefaultSortingTransformer(modelManager); + } + + private Dummy[] GetArray(string uri) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + return GetTransformer().Sort(_fixturesQuery, request).ToArray(); + } + + private void RunTransformAndExpectFailure(string uri, string expectedMessage) + { + Action action = () => + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + + // ReSharper disable once UnusedVariable + var result = GetTransformer().Sort(_fixturesQuery, request).ToArray(); + }; + action.ShouldThrow().Which.Message.Should().Be(expectedMessage); + } + + [TestMethod] + public void Sorts_by_attribute_ascending() + { + var array = GetArray("http://api.example.com/dummies?sort=%2BfirstName"); + array.Should().BeInAscendingOrder(d => d.FirstName); + } + + [TestMethod] + public void Sorts_by_attribute_descending() + { + var array = GetArray("http://api.example.com/dummies?sort=-firstName"); + array.Should().BeInDescendingOrder(d => d.FirstName); + } + + [TestMethod] + public void Sorts_by_two_ascending_attributes() + { + var array = GetArray("http://api.example.com/dummies?sort=%2BlastName,%2BfirstName"); + array.Should().ContainInOrder(_fixtures.OrderBy(d => d.LastName + d.FirstName)); + } + + [TestMethod] + public void Sorts_by_two_descending_attributes() + { + var array = GetArray("http://api.example.com/dummies?sort=-lastName,-firstName"); + array.Should().ContainInOrder(_fixtures.OrderByDescending(d => d.LastName + d.FirstName)); + } + + [TestMethod] + public void Returns_400_if_sort_argument_is_empty() + { + RunTransformAndExpectFailure("http://api.example.com/dummies?sort=", "The sort expression \"\" is invalid."); + } + + [TestMethod] + public void Returns_400_if_sort_argument_is_whitespace() + { + RunTransformAndExpectFailure("http://api.example.com/dummies?sort= ", "The sort expression \"\" is invalid."); + } + + [TestMethod] + public void Returns_400_if_property_name_is_missing() + { + RunTransformAndExpectFailure("http://api.example.com/dummies?sort=%2B", "The property name is missing."); + } + + [TestMethod] + public void Returns_400_if_property_name_is_whitespace() + { + RunTransformAndExpectFailure("http://api.example.com/dummies?sort=%2B ", "The property name is missing."); + } + + [TestMethod] + public void Returns_400_if_no_property_exists() + { + RunTransformAndExpectFailure("http://api.example.com/dummies?sort=%2Bfoobar", "The attribute \"foobar\" does not exist on type \"dummies\"."); + } + + [TestMethod] + public void Returns_400_if_the_same_property_is_specified_more_than_once() + { + RunTransformAndExpectFailure("http://api.example.com/dummies?sort=%2BlastName,%2BlastName", "The attribute \"lastName\" was specified more than once."); + } + + [TestMethod] + public void Returns_400_if_sort_argument_doesnt_start_with_plus_or_minus() + { + RunTransformAndExpectFailure("http://api.example.com/dummies?sort=lastName", "The sort expression \"lastName\" does not begin with a direction indicator (+ or -)."); + } + } +} diff --git a/JSONAPI.Tests/ActionFilters/EnableSortingAttributeTests.cs b/JSONAPI.Tests/ActionFilters/EnableSortingAttributeTests.cs deleted file mode 100644 index 03a86adf..00000000 --- a/JSONAPI.Tests/ActionFilters/EnableSortingAttributeTests.cs +++ /dev/null @@ -1,182 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Web.Http; -using System.Web.Http.Controllers; -using System.Web.Http.Filters; -using FluentAssertions; -using JSONAPI.ActionFilters; -using JSONAPI.Core; -using JSONAPI.Json; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace JSONAPI.Tests.ActionFilters -{ - [TestClass] - public class EnableSortingAttributeTests - { - private class Dummy - { - public string Id { get; set; } - - public string FirstName { get; set; } - - public string LastName { get; set; } - } - - private IList _fixtures; - private IQueryable _fixturesQuery; - - [TestInitialize] - public void SetupFixtures() - { - _fixtures = new List - { - new Dummy { Id = "1", FirstName = "Thomas", LastName = "Paine" }, - new Dummy { Id = "2", FirstName = "Samuel", LastName = "Adams" }, - new Dummy { Id = "3", FirstName = "George", LastName = "Washington"}, - new Dummy { Id = "4", FirstName = "Thomas", LastName = "Jefferson" }, - new Dummy { Id = "5", FirstName = "Martha", LastName = "Washington"}, - new Dummy { Id = "6", FirstName = "Abraham", LastName = "Lincoln" }, - new Dummy { Id = "7", FirstName = "Andrew", LastName = "Jackson" }, - new Dummy { Id = "8", FirstName = "Andrew", LastName = "Johnson" }, - new Dummy { Id = "8", FirstName = "William", LastName = "Harrison" } - }; - _fixturesQuery = _fixtures.AsQueryable(); - } - - private HttpActionExecutedContext CreateActionExecutedContext(IModelManager modelManager, string uri) - { - var formatter = new JsonApiFormatter(modelManager); - - var httpContent = new ObjectContent(typeof (IQueryable), _fixturesQuery, 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 HttpResponseMessage GetActionFilterResponse(string uri) - { - var modelManager = new ModelManager(new PluralizationService(new Dictionary - { - { "Dummy", "Dummies" } - })); - modelManager.RegisterResourceType(typeof(Dummy)); - - var filter = new EnableSortingAttribute(modelManager); - - var context = CreateActionExecutedContext(modelManager, uri); - - filter.OnActionExecuted(context); - - return context.Response; - } - - private T[] GetArray(string uri) - { - var response = GetActionFilterResponse(uri); - response.StatusCode.Should().Be(HttpStatusCode.OK); - - var returnedContent = (ObjectContent)response.Content; - var returnedQueryable = (IQueryable)returnedContent.Value; - return returnedQueryable.ToArray(); - } - - private void Expect400(string uri, string expectedMessage) - { - Action action = () => - { - GetActionFilterResponse(uri); - }; - var response = action.ShouldThrow().Which.Response; - - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - var value = (HttpError)((ObjectContent)response.Content).Value; - value.Message.Should().Be(expectedMessage); - } - - [TestMethod] - public void Sorts_by_attribute_ascending() - { - var array = GetArray("http://api.example.com/dummies?sort=%2BfirstName"); - array.Should().BeInAscendingOrder(d => d.FirstName); - } - - [TestMethod] - public void Sorts_by_attribute_descending() - { - var array = GetArray("http://api.example.com/dummies?sort=-firstName"); - array.Should().BeInDescendingOrder(d => d.FirstName); - } - - [TestMethod] - public void Sorts_by_two_ascending_attributes() - { - var array = GetArray("http://api.example.com/dummies?sort=%2BlastName,%2BfirstName"); - array.Should().ContainInOrder(_fixtures.OrderBy(d => d.LastName + d.FirstName)); - } - - [TestMethod] - public void Sorts_by_two_descending_attributes() - { - var array = GetArray("http://api.example.com/dummies?sort=-lastName,-firstName"); - array.Should().ContainInOrder(_fixtures.OrderByDescending(d => d.LastName + d.FirstName)); - } - - [TestMethod] - public void Returns_400_if_sort_argument_is_empty() - { - Expect400("http://api.example.com/dummies?sort=", "The sort expression \"\" is invalid."); - } - - [TestMethod] - public void Returns_400_if_sort_argument_is_whitespace() - { - Expect400("http://api.example.com/dummies?sort= ", "The sort expression \"\" is invalid."); - } - - [TestMethod] - public void Returns_400_if_property_name_is_missing() - { - Expect400("http://api.example.com/dummies?sort=%2B", "The property name is missing."); - } - - [TestMethod] - public void Returns_400_if_property_name_is_whitespace() - { - Expect400("http://api.example.com/dummies?sort=%2B ", "The property name is missing."); - } - - [TestMethod] - public void Returns_400_if_no_property_exists() - { - Expect400("http://api.example.com/dummies?sort=%2Bfoobar", "The attribute \"foobar\" does not exist on type \"dummies\"."); - } - - [TestMethod] - public void Returns_400_if_the_same_property_is_specified_more_than_once() - { - Expect400("http://api.example.com/dummies?sort=%2BlastName,%2BlastName", "The attribute \"lastName\" was specified more than once."); - } - - [TestMethod] - public void Returns_400_if_sort_argument_doesnt_start_with_plus_or_minus() - { - Expect400("http://api.example.com/dummies?sort=lastName", "The sort expression \"lastName\" does not begin with a direction indicator (+ or -)."); - } - } -} diff --git a/JSONAPI.Tests/ActionFilters/QueryableTransformerTestsBase.cs b/JSONAPI.Tests/ActionFilters/QueryableTransformerTestsBase.cs new file mode 100644 index 00000000..6a621587 --- /dev/null +++ b/JSONAPI.Tests/ActionFilters/QueryableTransformerTestsBase.cs @@ -0,0 +1,15 @@ +using System.Linq; +using System.Net.Http; +using JSONAPI.ActionFilters; + +namespace JSONAPI.Tests.ActionFilters +{ + public abstract class QueryableTransformerTestsBase + { + internal IQueryable Transform(IQueryableFilteringTransformer filteringTransformer, IQueryable query, string uri) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + return filteringTransformer.Filter(query, request); + } + } +} diff --git a/JSONAPI.Tests/JSONAPI.Tests.csproj b/JSONAPI.Tests/JSONAPI.Tests.csproj index 4f377db4..141efa70 100644 --- a/JSONAPI.Tests/JSONAPI.Tests.csproj +++ b/JSONAPI.Tests/JSONAPI.Tests.csproj @@ -79,8 +79,9 @@ - - + + + diff --git a/JSONAPI.TodoMVC.API/Startup.cs b/JSONAPI.TodoMVC.API/Startup.cs index 72d785b5..fa3c1869 100644 --- a/JSONAPI.TodoMVC.API/Startup.cs +++ b/JSONAPI.TodoMVC.API/Startup.cs @@ -1,10 +1,6 @@ using System.Web.Http; -using System.Web.Http.Dispatcher; -using JSONAPI.ActionFilters; using JSONAPI.Core; -using JSONAPI.EntityFramework.ActionFilters; -using JSONAPI.Http; -using JSONAPI.Json; +using JSONAPI.EntityFramework; using JSONAPI.TodoMVC.API.Models; using Owin; using PluralizationService = JSONAPI.Core.PluralizationService; @@ -22,28 +18,21 @@ public void Configuration(IAppBuilder app) private static HttpConfiguration GetWebApiConfiguration() { var pluralizationService = new PluralizationService(); + pluralizationService.AddMapping("todo", "todos"); - var config = new HttpConfiguration(); + var httpConfig = new HttpConfiguration(); - var modelManager = new ModelManager(pluralizationService); - modelManager.RegisterResourceType(typeof(Todo)); - - var formatter = new JsonApiFormatter(modelManager); - config.Formatters.Clear(); - config.Formatters.Add(formatter); - - // Global filters - config.Filters.Add(new EnumerateQueryableAsyncAttribute()); - config.Filters.Add(new EnableSortingAttribute(modelManager)); - config.Filters.Add(new EnableFilteringAttribute(modelManager)); - - // Override controller selector - config.Services.Replace(typeof(IHttpControllerSelector), new PascalizedControllerSelector(config)); + // Configure JSON API + new JsonApiConfiguration() + .PluralizeResourceTypesWith(pluralizationService) + .UseEntityFramework() + .RegisterResourceType(typeof(Todo)) + .Apply(httpConfig); // Web API routes - config.Routes.MapHttpRoute("DefaultApi", "{controller}/{id}", new { id = RouteParameter.Optional }); + httpConfig.Routes.MapHttpRoute("DefaultApi", "{controller}/{id}", new { id = RouteParameter.Optional }); - return config; + return httpConfig; } } } \ No newline at end of file diff --git a/JSONAPI/ActionFilters/EnableFilteringAttribute.cs b/JSONAPI/ActionFilters/DefaultFilteringTransformer.cs similarity index 92% rename from JSONAPI/ActionFilters/EnableFilteringAttribute.cs rename to JSONAPI/ActionFilters/DefaultFilteringTransformer.cs index 94427193..d29890f6 100644 --- a/JSONAPI/ActionFilters/EnableFilteringAttribute.cs +++ b/JSONAPI/ActionFilters/DefaultFilteringTransformer.cs @@ -5,21 +5,36 @@ using System.Net.Http; using System.Reflection; using System.Web.Http; -using System.Web.Http.Filters; using JSONAPI.Core; namespace JSONAPI.ActionFilters { - public class EnableFilteringAttribute : ActionFilterAttribute + /// + /// This transformer filters an IQueryable payload based on query-string values. + /// + public class DefaultFilteringTransformer : IQueryableFilteringTransformer { private readonly IModelManager _modelManager; - public EnableFilteringAttribute(IModelManager modelManager) + /// + /// Creates a new FilteringQueryableTransformer + /// + /// The model manager used to look up registered type information. + public DefaultFilteringTransformer(IModelManager modelManager) { _modelManager = modelManager; } + public IQueryable Filter(IQueryable query, HttpRequestMessage request) + { + var parameter = Expression.Parameter(typeof(T)); + var bodyExpr = GetPredicateBody(request, parameter); + var lambdaExpr = Expression.Lambda>(bodyExpr, parameter); + return query.Where(lambdaExpr); + } + // Borrowed from http://stackoverflow.com/questions/3631547/select-right-generic-method-with-reflection + // ReSharper disable once UnusedMember.Local private readonly Lazy _whereMethod = new Lazy(() => typeof(Queryable).GetMethods() .Where(x => x.Name == "Where") @@ -39,30 +54,6 @@ public EnableFilteringAttribute(IModelManager modelManager) .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.Request, 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 Expression GetPredicateBody(HttpRequestMessage request, ParameterExpression param) { Expression workingExpr = null; @@ -74,6 +65,10 @@ private Expression GetPredicateBody(HttpRequestMessage request, ParameterExpress if (String.IsNullOrWhiteSpace(queryPair.Key)) continue; + // TODO: Filtering needs to change to use the `filter` query parameter so that sorting no longer presents a conflict. + if (queryPair.Key == "sort") + continue; + ModelProperty modelProperty; try { @@ -100,12 +95,16 @@ private Expression GetPredicateBody(HttpRequestMessage request, ParameterExpress if (relationshipModelProperty != null) expr = GetPredicateBodyForRelationship(relationshipModelProperty, queryValue, param); + if (expr == null) throw new HttpResponseException(HttpStatusCode.BadRequest); + workingExpr = workingExpr == null ? expr : Expression.AndAlso(workingExpr, expr); } return workingExpr ?? Expression.Constant(true); // No filters, so return everything } + // ReSharper disable once FunctionComplexityOverflow + // TODO: should probably break this method up private Expression GetPredicateBodyForField(FieldModelProperty modelProperty, string queryValue, ParameterExpression param) { var prop = modelProperty.Property; @@ -427,4 +426,4 @@ private static Expression GetEnumPropertyExpression(int? value, PropertyInfo pro return Expression.Equal(castedPropertyExpr, castedValueExpr); } } -} \ No newline at end of file +} diff --git a/JSONAPI/ActionFilters/DefaultSortingTransformer.cs b/JSONAPI/ActionFilters/DefaultSortingTransformer.cs new file mode 100644 index 00000000..ed4a7db0 --- /dev/null +++ b/JSONAPI/ActionFilters/DefaultSortingTransformer.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Net.Http; +using System.Reflection; +using JSONAPI.Core; + +namespace JSONAPI.ActionFilters +{ + /// + /// This transform sorts an IQueryable payload according to query parameters. + /// + public class DefaultSortingTransformer : IQueryableSortingTransformer + { + private readonly IModelManager _modelManager; + + /// + /// Creates a new SortingQueryableTransformer + /// + /// The model manager used to look up registered type information. + public DefaultSortingTransformer(IModelManager modelManager) + { + _modelManager = modelManager; + } + + private const string SortQueryParamKey = "sort"; + + public IQueryable Sort(IQueryable query, HttpRequestMessage request) + { + var queryParams = request.GetQueryNameValuePairs(); + var sortParam = queryParams.FirstOrDefault(kvp => kvp.Key == SortQueryParamKey); + if (sortParam.Key != SortQueryParamKey) return query; // Nothing to sort here, move along + + var selectors = new List>>>(); + + var usedProperties = new Dictionary(); + + var sortExpressions = sortParam.Value.Split(','); + foreach (var sortExpression in sortExpressions) + { + if (string.IsNullOrWhiteSpace(sortExpression)) + throw new QueryableTransformException(string.Format("The sort expression \"{0}\" is invalid.", sortExpression)); + + var ascending = sortExpression[0] == '+'; + var descending = sortExpression[0] == '-'; + if (!ascending && !descending) + throw new QueryableTransformException(string.Format("The sort expression \"{0}\" does not begin with a direction indicator (+ or -).", sortExpression)); + + var propertyName = sortExpression.Substring(1); + if (string.IsNullOrWhiteSpace(propertyName)) + throw new QueryableTransformException("The property name is missing."); + + var modelProperty = _modelManager.GetPropertyForJsonKey(typeof(T), propertyName); + + if (modelProperty == null) + throw new QueryableTransformException(string.Format("The attribute \"{0}\" does not exist on type \"{1}\".", + propertyName, _modelManager.GetResourceTypeNameForType(typeof (T)))); + + var property = modelProperty.Property; + + if (usedProperties.ContainsKey(property)) + throw new QueryableTransformException(string.Format("The attribute \"{0}\" was specified more than once.", propertyName)); + + usedProperties[property] = null; + + var paramExpr = Expression.Parameter(typeof (T)); + var propertyExpr = Expression.Property(paramExpr, property); + var selector = Expression.Lambda>(propertyExpr, paramExpr); + + selectors.Add(Tuple.Create(ascending, selector)); + } + + var firstSelector = selectors.First(); + + IOrderedQueryable workingQuery = + firstSelector.Item1 + ? query.OrderBy(firstSelector.Item2) + : query.OrderByDescending(firstSelector.Item2); + + return selectors.Skip(1).Aggregate(workingQuery, + (current, selector) => + selector.Item1 ? current.ThenBy(selector.Item2) : current.ThenByDescending(selector.Item2)); + } + } +} diff --git a/JSONAPI/ActionFilters/EnableSortingAttribute.cs b/JSONAPI/ActionFilters/EnableSortingAttribute.cs deleted file mode 100644 index 5ebb967a..00000000 --- a/JSONAPI/ActionFilters/EnableSortingAttribute.cs +++ /dev/null @@ -1,128 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Net; -using System.Net.Http; -using System.Reflection; -using System.Web.Http; -using System.Web.Http.Filters; -using JSONAPI.Core; - -namespace JSONAPI.ActionFilters -{ - /// - /// Sorts the IQueryable response content according to json-api - /// - public class EnableSortingAttribute : ActionFilterAttribute - { - private const string SortQueryParamKey = "sort"; - - private readonly IModelManager _modelManager; - - /// The model manager to use to look up model properties by json key name - public EnableSortingAttribute(IModelManager modelManager) - { - _modelManager = modelManager; - } - - private readonly Lazy _openMakeOrderedQueryMethod = - new Lazy(() => typeof(EnableSortingAttribute).GetMethod("MakeOrderedQuery", BindingFlags.NonPublic | BindingFlags.Instance)); - - public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) - { - if (actionExecutedContext.Response != null) - { - var objectContent = actionExecutedContext.Response.Content as ObjectContent; - if (objectContent == null) return; - - var objectType = objectContent.ObjectType; - if (!objectType.IsGenericType || objectType.GetGenericTypeDefinition() != typeof (IQueryable<>)) return; - - var queryParams = actionExecutedContext.Request.GetQueryNameValuePairs(); - var sortParam = queryParams.FirstOrDefault(kvp => kvp.Key == SortQueryParamKey); - if (sortParam.Key != SortQueryParamKey) return; - - var queryableElementType = objectType.GenericTypeArguments[0]; - var makeOrderedQueryMethod = _openMakeOrderedQueryMethod.Value.MakeGenericMethod(queryableElementType); - - try - { - var orderedQuery = makeOrderedQueryMethod.Invoke(this, new[] {objectContent.Value, sortParam.Value}); - - actionExecutedContext.Response.Content = new ObjectContent(objectType, orderedQuery, - objectContent.Formatter); - } - catch (TargetInvocationException ex) - { - var statusCode = ex.InnerException is SortingException - ? HttpStatusCode.BadRequest - : HttpStatusCode.InternalServerError; - throw new HttpResponseException( - actionExecutedContext.Request.CreateErrorResponse(statusCode, ex.InnerException.Message)); - } - } - } - - // ReSharper disable once UnusedMember.Local - private IQueryable MakeOrderedQuery(IQueryable sourceQuery, string sortParam) - { - var selectors = new List>>>(); - - var usedProperties = new Dictionary(); - - var sortExpressions = sortParam.Split(','); - foreach (var sortExpression in sortExpressions) - { - if (string.IsNullOrWhiteSpace(sortExpression)) - throw new SortingException(string.Format("The sort expression \"{0}\" is invalid.", sortExpression)); - - var ascending = sortExpression[0] == '+'; - var descending = sortExpression[0] == '-'; - if (!ascending && !descending) - throw new SortingException(string.Format("The sort expression \"{0}\" does not begin with a direction indicator (+ or -).", sortExpression)); - - var propertyName = sortExpression.Substring(1); - if (string.IsNullOrWhiteSpace(propertyName)) - throw new SortingException("The property name is missing."); - - var property = _modelManager.GetPropertyForJsonKey(typeof(T), propertyName); - - if (property == null) - throw new SortingException(string.Format("The attribute \"{0}\" does not exist on type \"{1}\".", - propertyName, _modelManager.GetResourceTypeNameForType(typeof (T)))); - - if (usedProperties.ContainsKey(property)) - throw new SortingException(string.Format("The attribute \"{0}\" was specified more than once.", propertyName)); - - usedProperties[property] = null; - - var paramExpr = Expression.Parameter(typeof (T)); - var propertyExpr = Expression.Property(paramExpr, property.Property); - var selector = Expression.Lambda>(propertyExpr, paramExpr); - - selectors.Add(Tuple.Create(ascending, selector)); - } - - var firstSelector = selectors.First(); - - IOrderedQueryable workingQuery = - firstSelector.Item1 - ? sourceQuery.OrderBy(firstSelector.Item2) - : sourceQuery.OrderByDescending(firstSelector.Item2); - - return selectors.Skip(1).Aggregate(workingQuery, - (current, selector) => - selector.Item1 ? current.ThenBy(selector.Item2) : current.ThenByDescending(selector.Item2)); - } - } - - internal class SortingException : Exception - { - public SortingException(string message) - : base(message) - { - - } - } -} diff --git a/JSONAPI/ActionFilters/IQueryableEnumerationTransformer.cs b/JSONAPI/ActionFilters/IQueryableEnumerationTransformer.cs new file mode 100644 index 00000000..5fd785fd --- /dev/null +++ b/JSONAPI/ActionFilters/IQueryableEnumerationTransformer.cs @@ -0,0 +1,22 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace JSONAPI.ActionFilters +{ + /// + /// Provides a service to asynchronously materialize the results of an IQueryable into + /// a concrete in-memory representation for serialization. + /// + public interface IQueryableEnumerationTransformer + { + /// + /// Enumerates the specified query. + /// + /// The query to enumerate + /// The request's cancellation token. If this token is cancelled during enumeration, enumeration must halt. + /// The queryable element type + /// A task yielding the enumerated results of the query + Task Enumerate(IQueryable query, CancellationToken cancellationToken); + } +} diff --git a/JSONAPI/ActionFilters/IQueryableFilteringTransformer.cs b/JSONAPI/ActionFilters/IQueryableFilteringTransformer.cs new file mode 100644 index 00000000..845644e7 --- /dev/null +++ b/JSONAPI/ActionFilters/IQueryableFilteringTransformer.cs @@ -0,0 +1,20 @@ +using System.Linq; +using System.Net.Http; + +namespace JSONAPI.ActionFilters +{ + /// + /// Service for filtering an IQueryable according to an HTTP request. + /// + public interface IQueryableFilteringTransformer + { + /// + /// Filters the provided queryable based on information from the request message. + /// + /// The input query + /// The request message + /// The element type of the query + /// The filtered query + IQueryable Filter(IQueryable query, HttpRequestMessage request); + } +} diff --git a/JSONAPI/ActionFilters/IQueryableSortingTransformer.cs b/JSONAPI/ActionFilters/IQueryableSortingTransformer.cs new file mode 100644 index 00000000..394400ec --- /dev/null +++ b/JSONAPI/ActionFilters/IQueryableSortingTransformer.cs @@ -0,0 +1,20 @@ +using System.Linq; +using System.Net.Http; + +namespace JSONAPI.ActionFilters +{ + /// + /// Service for sorting an IQueryable according to an HTTP request. + /// + public interface IQueryableSortingTransformer + { + /// + /// Sorts the provided queryable based on information from the request message. + /// + /// The input query + /// The request message + /// The element type of the query + /// The sorted query + IQueryable Sort(IQueryable query, HttpRequestMessage request); + } +} \ No newline at end of file diff --git a/JSONAPI/ActionFilters/JsonApiQueryableAttribute.cs b/JSONAPI/ActionFilters/JsonApiQueryableAttribute.cs new file mode 100644 index 00000000..714b3967 --- /dev/null +++ b/JSONAPI/ActionFilters/JsonApiQueryableAttribute.cs @@ -0,0 +1,104 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using System.Web.Http; +using System.Web.Http.Filters; + +namespace JSONAPI.ActionFilters +{ + /// + /// This is an action filter that you can insert into your Web API pipeline to perform various transforms + /// on IQueryable payloads. + /// + public class JsonApiQueryableAttribute : ActionFilterAttribute + { + private readonly IQueryableEnumerationTransformer _enumerationTransformer; + private readonly IQueryableFilteringTransformer _filteringTransformer; + private readonly IQueryableSortingTransformer _sortingTransformer; + + /// + /// Creates a new JsonApiQueryableAttribute. + /// + /// The transform to be used for enumerating IQueryable payloads. + /// The transform to be used for filtering IQueryable payloads + /// The transform to be used for sorting IQueryable payloads. + public JsonApiQueryableAttribute( + IQueryableEnumerationTransformer enumerationTransformer, + IQueryableFilteringTransformer filteringTransformer = null, + IQueryableSortingTransformer sortingTransformer = null) + { + _sortingTransformer = sortingTransformer; + _enumerationTransformer = enumerationTransformer; + _filteringTransformer = filteringTransformer; + } + + private readonly Lazy _openApplyTransformsMethod = + new Lazy(() => typeof(JsonApiQueryableAttribute).GetMethod("ApplyTransforms", BindingFlags.NonPublic | BindingFlags.Instance)); + + public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken) + { + 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 applyTransformsMethod = _openApplyTransformsMethod.Value.MakeGenericMethod(queryableElementType); + + try + { + dynamic materializedQueryTask; + try + { + materializedQueryTask = applyTransformsMethod.Invoke(this, + new[] {objectContent.Value, actionExecutedContext.Request, cancellationToken}); + } + catch (TargetInvocationException ex) + { + throw ex.InnerException; + } + + var materializedResults = await materializedQueryTask; + + actionExecutedContext.Response.Content = new ObjectContent( + materializedResults.GetType(), + materializedResults, + objectContent.Formatter); + } + catch (QueryableTransformException ex) + { + throw new HttpResponseException( + actionExecutedContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, ex.Message)); + } + catch (Exception ex) + { + throw new HttpResponseException( + actionExecutedContext.Request.CreateErrorResponse(HttpStatusCode.InternalServerError, ex.Message)); + + } + } + } + } + } + + // ReSharper disable once UnusedMember.Local + private async Task ApplyTransforms(IQueryable query, HttpRequestMessage request, + CancellationToken cancellationToken) + { + if (_filteringTransformer != null) + query = _filteringTransformer.Filter(query, request); + + if (_sortingTransformer != null) + query = _sortingTransformer.Sort(query, request); + + return await _enumerationTransformer.Enumerate(query, cancellationToken); + } + } +} diff --git a/JSONAPI/ActionFilters/QueryableTransformException.cs b/JSONAPI/ActionFilters/QueryableTransformException.cs new file mode 100644 index 00000000..47a0485f --- /dev/null +++ b/JSONAPI/ActionFilters/QueryableTransformException.cs @@ -0,0 +1,13 @@ +using System; + +namespace JSONAPI.ActionFilters +{ + internal class QueryableTransformException : Exception + { + public QueryableTransformException(string message) + : base(message) + { + + } + } +} diff --git a/JSONAPI/ActionFilters/SynchronousEnumerationTransformer.cs b/JSONAPI/ActionFilters/SynchronousEnumerationTransformer.cs new file mode 100644 index 00000000..ea084f44 --- /dev/null +++ b/JSONAPI/ActionFilters/SynchronousEnumerationTransformer.cs @@ -0,0 +1,17 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace JSONAPI.ActionFilters +{ + /// + /// Synchronously enumerates an IQueryable + /// + public class SynchronousEnumerationTransformer : IQueryableEnumerationTransformer + { + public Task Enumerate(IQueryable query, CancellationToken cancellationToken) + { + return Task.FromResult(query.ToArray()); + } + } +} diff --git a/JSONAPI/Core/JsonApiConfiguration.cs b/JSONAPI/Core/JsonApiConfiguration.cs new file mode 100644 index 00000000..df092139 --- /dev/null +++ b/JSONAPI/Core/JsonApiConfiguration.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Web.Http; +using System.Web.Http.Dispatcher; +using System.Web.Http.Filters; +using JSONAPI.ActionFilters; +using JSONAPI.Http; +using JSONAPI.Json; + +namespace JSONAPI.Core +{ + /// + /// Configuration API for JSONAPI.NET + /// + public class JsonApiConfiguration + { + private bool _enableFiltering; + private bool _enableSorting; + private IQueryableFilteringTransformer _filteringTransformer; + private IQueryableSortingTransformer _sortingTransformer; + private IQueryableEnumerationTransformer _enumerationTransformer; + private IPluralizationService _pluralizationService; + private readonly IList _resourceTypes; + + /// + /// Creates a new configuration + /// + public JsonApiConfiguration() + { + _enableFiltering = true; + _enableSorting = true; + _filteringTransformer = null; + _sortingTransformer = null; + _enumerationTransformer = null; + _resourceTypes = new List(); + } + + /// + /// Disable filtering of IQueryables in GET methods + /// + /// The same configuration object the method was called on. + public JsonApiConfiguration DisableFiltering() + { + _enableFiltering = false; + return this; + } + + /// + /// Disable sorting of IQueryables in GET methods + /// + /// The same configuration object the method was called on. + public JsonApiConfiguration DisableSorting() + { + _enableSorting = false; + return this; + } + + /// + /// Specifies a filtering transformer to use for filtering IQueryable response payloads. + /// + /// The filtering transformer. + /// The same configuration object the method was called on. + public JsonApiConfiguration FilterWith(IQueryableFilteringTransformer filteringTransformer) + { + _filteringTransformer = filteringTransformer; + return this; + } + + /// + /// Specifies a sorting transformer to use for sorting IQueryable response payloads. + /// + /// The sorting transformer. + /// The same configuration object the method was called on. + public JsonApiConfiguration SortWith(IQueryableSortingTransformer sortingTransformer) + { + _sortingTransformer = sortingTransformer; + return this; + } + + /// + /// Specifies an enumeration transformer to use for enumerating IQueryable response payloads. + /// + /// The enumeration transformer. + /// The same configuration object the method was called on. + public JsonApiConfiguration EnumerateQueriesWith(IQueryableEnumerationTransformer enumerationTransformer) + { + _enumerationTransformer = enumerationTransformer; + return this; + } + + /// + /// Specifies a service to provide pluralizations of resource type names. + /// + /// The service + /// The same configuration object that was passed in + public JsonApiConfiguration PluralizeResourceTypesWith(IPluralizationService pluralizationService) + { + _pluralizationService = pluralizationService; + return this; + } + + /// + /// Registers a resource type for use with the model manager. + /// + /// + /// + public JsonApiConfiguration RegisterResourceType(Type type) + { + _resourceTypes.Add(type); + return this; + } + + /// + /// Applies the running configuration to an HttpConfiguration instance + /// + /// The HttpConfiguration to apply this JsonApiConfiguration to + public void Apply(HttpConfiguration httpConfig) + { + var pluralizationService = _pluralizationService ?? new PluralizationService(); + var modelManager = new ModelManager(pluralizationService); + foreach (var resourceType in _resourceTypes) + { + modelManager.RegisterResourceType(resourceType); + } + + IQueryableFilteringTransformer filteringTransformer = null; + if (_enableFiltering) + filteringTransformer = _filteringTransformer ?? new DefaultFilteringTransformer(modelManager); + + IQueryableSortingTransformer sortingTransformer = null; + if (_enableSorting) + sortingTransformer = _sortingTransformer ?? new DefaultSortingTransformer(modelManager); + + IQueryableEnumerationTransformer enumerationTransformer = + _enumerationTransformer ?? new SynchronousEnumerationTransformer(); + + var formatter = new JsonApiFormatter(modelManager); + + httpConfig.Formatters.Clear(); + httpConfig.Formatters.Add(formatter); + + httpConfig.Filters.Add(new JsonApiQueryableAttribute(enumerationTransformer, filteringTransformer, sortingTransformer)); + + httpConfig.Services.Replace(typeof (IHttpControllerSelector), + new PascalizedControllerSelector(httpConfig)); + } + } +} diff --git a/JSONAPI/Extensions/QueryableExtensions.cs b/JSONAPI/Extensions/QueryableExtensions.cs new file mode 100644 index 00000000..e6aca042 --- /dev/null +++ b/JSONAPI/Extensions/QueryableExtensions.cs @@ -0,0 +1,14 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Net.Http; +using System.Reflection; +using System.Web.Http.Filters; +using JSONAPI.Core; + +namespace JSONAPI.Extensions +{ + internal static class QueryableExtensions + { + } +} diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj index 4feb31fb..b64e686d 100644 --- a/JSONAPI/JSONAPI.csproj +++ b/JSONAPI/JSONAPI.csproj @@ -66,8 +66,14 @@ - - + + + + + + + + @@ -77,9 +83,11 @@ + + diff --git a/JSONAPI/Properties/AssemblyInfo.cs b/JSONAPI/Properties/AssemblyInfo.cs index f61caa98..492b29a6 100644 --- a/JSONAPI/Properties/AssemblyInfo.cs +++ b/JSONAPI/Properties/AssemblyInfo.cs @@ -23,6 +23,7 @@ [assembly: Guid("5b46482f-733f-42bf-b507-37767a6bb948")] [assembly: InternalsVisibleTo("JSONAPI.Tests")] +[assembly: InternalsVisibleTo("JSONAPI.EntityFramework")] [assembly: InternalsVisibleTo("JSONAPI.EntityFramework.Tests")] // This assembly is the default dynamic assembly generated Castle DynamicProxy,