From 5717f3068f2ff6792670d83801deb5427064ce00 Mon Sep 17 00:00:00 2001 From: Josh Hubers Date: Thu, 25 Apr 2019 12:04:33 -0400 Subject: [PATCH 1/3] Pass FilterQuery to GetQueryFilters --- .../Resources/UserResource.cs | 21 +++++ .../Data/DefaultEntityRepository.cs | 2 +- .../Models/ResourceDefinition.cs | 2 +- .../ResourceDefinitions/QueryFiltersTests.cs | 86 +++++++++++++++++++ 4 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/QueryFiltersTests.cs diff --git a/src/Examples/JsonApiDotNetCoreExample/Resources/UserResource.cs b/src/Examples/JsonApiDotNetCoreExample/Resources/UserResource.cs index 030bc4eaa4..0f9c473d17 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Resources/UserResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Resources/UserResource.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; +using System.Linq; using JsonApiDotNetCore.Models; using JsonApiDotNetCoreExample.Models; +using JsonApiDotNetCore.Internal.Query; namespace JsonApiDotNetCoreExample.Resources { @@ -8,5 +10,24 @@ public class UserResource : ResourceDefinition { protected override List OutputAttrs() => Remove(user => user.Password); + + public override QueryFilters GetQueryFilters() + { + return new QueryFilters + { + { "first-character", (users, queryFilter) => FirstCharacterFilter(users, queryFilter) } + }; + } + + private IQueryable FirstCharacterFilter(IQueryable users, FilterQuery filterQuery) + { + switch(filterQuery.Operation) + { + case "lt": + return users.Where(u => u.Username[0] < filterQuery.Value[0]); + default: + return users.Where(u => u.Username[0] == filterQuery.Value[0]); + } + } } } diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 781102fcdf..43388e459a 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -108,7 +108,7 @@ public virtual IQueryable Filter(IQueryable entities, FilterQu var defaultQueryFilters = _resourceDefinition.GetQueryFilters(); if (defaultQueryFilters != null && defaultQueryFilters.TryGetValue(filterQuery.Attribute, out var defaultQueryFilter) == true) { - return defaultQueryFilter(entities, filterQuery.Value); + return defaultQueryFilter(entities, filterQuery); } } diff --git a/src/JsonApiDotNetCore/Models/ResourceDefinition.cs b/src/JsonApiDotNetCore/Models/ResourceDefinition.cs index cb4894ce27..77461aba42 100644 --- a/src/JsonApiDotNetCore/Models/ResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Models/ResourceDefinition.cs @@ -162,7 +162,7 @@ private List GetOutputAttrs() /// method signature. /// See for usage details. /// - public class QueryFilters : Dictionary, string, IQueryable>> { } + public class QueryFilters : Dictionary, FilterQuery, IQueryable>> { } /// /// Define a the default sort order if no sort key is provided. diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/QueryFiltersTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/QueryFiltersTests.cs new file mode 100644 index 0000000000..0dc2aefc84 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/QueryFiltersTests.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Bogus; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCoreExample.Data; +using JsonApiDotNetCoreExample.Models; +using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json; +using Xunit; + +namespace JsonApiDotNetCoreExampleTests.Acceptance +{ + [Collection("WebHostCollection")] + public class QueryFiltersTests + { + private TestFixture _fixture; + private AppDbContext _context; + private Faker _userFaker; + + public QueryFiltersTests(TestFixture fixture) + { + _fixture = fixture; + _context = fixture.GetService(); + _userFaker = new Faker() + .RuleFor(u => u.Username, f => f.Internet.UserName()) + .RuleFor(u => u.Password, f => f.Internet.Password()); + } + + [Fact] + public async Task FiltersWithCustomQueryFiltersEquals() + { + // Arrange + var user = _userFaker.Generate(); + var firstUsernameCharacter = user.Username[0]; + _context.Users.Add(user); + _context.SaveChanges(); + + var httpMethod = new HttpMethod("GET"); + var route = $"/api/v1/users?filter[first-character]=eq:{firstUsernameCharacter}"; + var request = new HttpRequestMessage(httpMethod, route); + + // Act + var response = await _fixture.Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var deserializedBody = _fixture.GetService().DeserializeList(body); + var usersWithFirstCharacter = _context.Users.Where(u => u.Username[0] == firstUsernameCharacter); + Assert.True(deserializedBody.All(u => u.Username[0] == firstUsernameCharacter)); + } + + [Fact] + public async Task FiltersWithCustomQueryFiltersLessThan() + { + // Arrange + var aUser = _userFaker.Generate(); + aUser.Username = "alfred"; + var zUser = _userFaker.Generate(); + zUser.Username = "zac"; + _context.Users.AddRange(aUser, zUser); + _context.SaveChanges(); + + var median = 'h'; + + var httpMethod = new HttpMethod("GET"); + var route = $"/api/v1/users?filter[first-character]=lt:{median}"; + var request = new HttpRequestMessage(httpMethod, route); + + // Act + var response = await _fixture.Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var deserializedBody = _fixture.GetService().DeserializeList(body); + var usersBeforeMedian = _context.Users.Where(u => u.Username[0] < median); + Assert.True(deserializedBody.All(u => u.Username[0] < median)); + } + } +} From 0a1f06d90ad1ef8d69a5f89e43461f556d3f3230 Mon Sep 17 00:00:00 2001 From: Josh Hubers Date: Thu, 25 Apr 2019 12:07:02 -0400 Subject: [PATCH 2/3] remove left over line --- .../Acceptance/ResourceDefinitions/QueryFiltersTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/QueryFiltersTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/QueryFiltersTests.cs index 0dc2aefc84..92589dd0b5 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/QueryFiltersTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/QueryFiltersTests.cs @@ -79,7 +79,6 @@ public async Task FiltersWithCustomQueryFiltersLessThan() Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); var deserializedBody = _fixture.GetService().DeserializeList(body); - var usersBeforeMedian = _context.Users.Where(u => u.Username[0] < median); Assert.True(deserializedBody.All(u => u.Username[0] < median)); } } From f9a85809961cfbc46d5817cbc3f63eaaec116154 Mon Sep 17 00:00:00 2001 From: Josh Hubers Date: Thu, 25 Apr 2019 12:10:29 -0400 Subject: [PATCH 3/3] Update docs for GetQueryFilters uses FilterQuery --- docs/usage/resources/resource-definitions.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/usage/resources/resource-definitions.md b/docs/usage/resources/resource-definitions.md index d1e5db5f2e..6810c97227 100644 --- a/docs/usage/resources/resource-definitions.md +++ b/docs/usage/resources/resource-definitions.md @@ -113,9 +113,9 @@ public class ItemResource : ResourceDefinition // handles queries like: ?filter[was-active-on]=2018-10-15T01:25:52Z public override QueryFilters GetQueryFilters() => new QueryFilters { - { "was-active-on", (items, value) => DateTime.TryParse(value, out dateValue) + { "was-active-on", (items, filter) => DateTime.TryParse(filter.Value, out dateValue) ? items.Where(i => i.Expired == null || dateValue < i.Expired) - : throw new JsonApiException(400, $"'{value}' is not a valid date.") + : throw new JsonApiException(400, $"'{filter.Value}' is not a valid date.") } }; } @@ -128,4 +128,4 @@ Prior to the introduction of auto-discovery, you needed to register the ```c# services.AddScoped, ItemResource>(); -``` \ No newline at end of file +```