From 2878aac6892eed07086ec9ea3f35aa72b2f21969 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Fri, 7 Apr 2017 09:05:34 -0500 Subject: [PATCH 01/12] feat(dasherized-route-conv): check if controller is JsonApiController --- .../Internal/DasherizedRoutingConvention.cs | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/JsonApiDotNetCore/Internal/DasherizedRoutingConvention.cs b/src/JsonApiDotNetCore/Internal/DasherizedRoutingConvention.cs index 7732a91250..97be6aee14 100644 --- a/src/JsonApiDotNetCore/Internal/DasherizedRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Internal/DasherizedRoutingConvention.cs @@ -1,5 +1,6 @@ // REF: https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.CustomRoutingConvention/NameSpaceRoutingConvention.cs // REF: https://github.com/aspnet/Mvc/issues/5691 +using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Extensions; using Microsoft.AspNetCore.Mvc.ApplicationModels; @@ -12,17 +13,28 @@ public DasherizedRoutingConvention(string nspace) { _namespace = nspace; } - + public void Apply(ApplicationModel application) { foreach (var controller in application.Controllers) - { - var template = $"{_namespace}/{controller.ControllerName.Dasherize()}"; - controller.Selectors[0].AttributeRouteModel = new AttributeRouteModel() + { + if (IsJsonApiController(controller)) { - Template = template - }; + var template = $"{_namespace}/{controller.ControllerName.Dasherize()}"; + controller.Selectors[0].AttributeRouteModel = new AttributeRouteModel() + { + Template = template + }; + } } } + + private bool IsJsonApiController(ControllerModel controller) + { + var controllerBaseType = controller.ControllerType.BaseType; + if(!controllerBaseType.IsConstructedGenericType) return false; + var genericTypeDefinition = controllerBaseType.GetGenericTypeDefinition(); + return (genericTypeDefinition == typeof(JsonApiController<,>) || genericTypeDefinition == typeof(JsonApiController<>)); + } } } From f9564f52fe0d7935961b7b1b33ceb3b4b62d18d9 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Fri, 7 Apr 2017 09:05:45 -0500 Subject: [PATCH 02/12] chore(csproj): bump package version --- src/JsonApiDotNetCore/JsonApiDotNetCore.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index 45684b0223..b2d2bd4a6c 100755 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -1,6 +1,6 @@  - 1.3.0 + 1.3.1 netcoreapp1.0 JsonApiDotNetCore JsonApiDotNetCore From 751f7684c8231a1ad1f8ff61d7840b533c082c91 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Sat, 8 Apr 2017 21:52:20 -0500 Subject: [PATCH 03/12] test(acceptance): can use non json-api controllers --- .../Controllers/TestValuesController.cs | 15 ++++++++ .../Extensibility/CustomControllerTests.cs | 34 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 src/JsonApiDotNetCoreExample/Controllers/TestValuesController.cs create mode 100644 test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs diff --git a/src/JsonApiDotNetCoreExample/Controllers/TestValuesController.cs b/src/JsonApiDotNetCoreExample/Controllers/TestValuesController.cs new file mode 100644 index 0000000000..fb8227c3b1 --- /dev/null +++ b/src/JsonApiDotNetCoreExample/Controllers/TestValuesController.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Mvc; + +namespace JsonApiDotNetCoreExample.Controllers +{ + [Route("[controller]")] + public class TestValuesController : Controller + { + [HttpGet] + public IActionResult Get() + { + var result = new string[] { "value" }; + return Ok(result); + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs new file mode 100644 index 0000000000..fe84817540 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs @@ -0,0 +1,34 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Xunit; +using JsonApiDotNetCoreExample; + +namespace JsonApiDotNetCoreExampleTests.Acceptance.Extensibility +{ + [Collection("WebHostCollection")] + public class CustomControllerTests + { + [Fact] + public async Task NonJsonApiControllers_DoNotUse_Dasherized_Routes() + { + // arrange + var builder = new WebHostBuilder() + .UseStartup(); + var httpMethod = new HttpMethod("GET"); + var route = $"testValues"; + + var server = new TestServer(builder); + var client = server.CreateClient(); + var request = new HttpRequestMessage(httpMethod, route); + + // act + var response = await client.SendAsync(request); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + } +} From 64c97238c55e97d69bee7ba15e1872d178e79718 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Sat, 8 Apr 2017 22:02:24 -0500 Subject: [PATCH 04/12] style --- .../Controllers/TestValuesController.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/JsonApiDotNetCoreExample/Controllers/TestValuesController.cs b/src/JsonApiDotNetCoreExample/Controllers/TestValuesController.cs index fb8227c3b1..3443d34b74 100644 --- a/src/JsonApiDotNetCoreExample/Controllers/TestValuesController.cs +++ b/src/JsonApiDotNetCoreExample/Controllers/TestValuesController.cs @@ -2,14 +2,14 @@ namespace JsonApiDotNetCoreExample.Controllers { - [Route("[controller]")] - public class TestValuesController : Controller - { - [HttpGet] - public IActionResult Get() + [Route("[controller]")] + public class TestValuesController : Controller { - var result = new string[] { "value" }; - return Ok(result); + [HttpGet] + public IActionResult Get() + { + var result = new string[] { "value" }; + return Ok(result); + } } - } } From 9089bbd03de40959a386cfd2e225a8345ff3b992 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Sun, 9 Apr 2017 11:05:50 -0500 Subject: [PATCH 05/12] test(acceptance): failing test for related attr filter --- src/JsonApiDotNetCoreExample/Models/Person.cs | 1 - .../Acceptance/Spec/AttributeFilterTests.cs | 45 +++++++++++++++++-- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/JsonApiDotNetCoreExample/Models/Person.cs b/src/JsonApiDotNetCoreExample/Models/Person.cs index 52b67347e9..04992a40a6 100644 --- a/src/JsonApiDotNetCoreExample/Models/Person.cs +++ b/src/JsonApiDotNetCoreExample/Models/Person.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs index 5c10196d1f..0a0a17d494 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs @@ -2,19 +2,17 @@ using System.Net.Http; using System.Threading.Tasks; using DotNetCoreDocs; -using DotNetCoreDocs.Models; using DotNetCoreDocs.Writers; using JsonApiDotNetCoreExample; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; -using Newtonsoft.Json; using Xunit; -using JsonApiDotNetCore.Internal; using JsonApiDotNetCoreExample.Data; using Bogus; using JsonApiDotNetCoreExample.Models; using JsonApiDotNetCore.Serialization; using System.Linq; +using Person = JsonApiDotNetCoreExample.Models.Person; namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec { @@ -23,13 +21,18 @@ public class AttributeFilterTests { private DocsFixture _fixture; private Faker _todoItemFaker; - + private readonly Faker _personFaker; + public AttributeFilterTests(DocsFixture fixture) { _fixture = fixture; _todoItemFaker = new Faker() .RuleFor(t => t.Description, f => f.Lorem.Sentence()) .RuleFor(t => t.Ordinal, f => f.Random.Number()); + + _personFaker = new Faker() + .RuleFor(p => p.FirstName, f => f.Name.FirstName()) + .RuleFor(p => p.LastName, f => f.Name.LastName()); } [Fact] @@ -63,5 +66,39 @@ public async Task Can_Filter_On_Guid_Properties() Assert.Equal(todoItem.Id, todoItemResponse.Id); Assert.Equal(todoItem.GuidProperty, todoItemResponse.GuidProperty); } + + + [Fact] + public async Task Can_Filter_On_Related_Attrs() + { + // arrange + var context = _fixture.GetService(); + var person = _personFaker.Generate(); + var todoItem = _todoItemFaker.Generate(); + todoItem.Owner = person; + context.TodoItems.Add(todoItem); + await context.SaveChangesAsync(); + + var builder = new WebHostBuilder() + .UseStartup(); + var httpMethod = new HttpMethod("GET"); + var route = $"/api/v1/todo-items?include[owner]&filter[owner.first-name]={person.FirstName}"; + var server = new TestServer(builder); + var client = server.CreateClient(); + var request = new HttpRequestMessage(httpMethod, route); + + // act + var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + var deserializedBody = _fixture + .GetService() + .DeserializeList(body); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotEmpty(deserializedBody); + foreach(var item in deserializedBody) + Assert.Equal(person.FirstName, item.Owner.FirstName); + } } } From 1cc467dc6415c07050881ec18cc74a71b0252278 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 11 Apr 2017 07:44:43 -0500 Subject: [PATCH 06/12] feat(filter-query): add RelatedAttrFilterQuery for relational filters --- .../Internal/Query/AttrFilterQuery.cs | 72 ++++++++----------- .../Internal/Query/BaseFilterQuery.cs | 18 +++++ .../Internal/Query/FilterQuery.cs | 1 + .../Internal/Query/RelatedAttrFilterQuery.cs | 51 +++++++++++++ 4 files changed, 100 insertions(+), 42 deletions(-) create mode 100644 src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs create mode 100644 src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs diff --git a/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs index 1a691d1d15..f45d384d72 100644 --- a/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs @@ -1,51 +1,39 @@ -using System; using System.Linq; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Services; namespace JsonApiDotNetCore.Internal.Query { - public class AttrFilterQuery - { - private readonly IJsonApiContext _jsonApiContext; - - public AttrFilterQuery( - IJsonApiContext jsonApiCopntext, - FilterQuery filterQuery) - { - _jsonApiContext = jsonApiCopntext; - - var attribute = GetAttribute(filterQuery.Key); - - if (attribute == null) - throw new JsonApiException("400", $"{filterQuery.Key} is not a valid property."); - - FilteredAttribute = attribute; - PropertyValue = filterQuery.Value; - FilterOperation = GetFilterOperation(filterQuery.Operation); - } - - public AttrAttribute FilteredAttribute { get; set; } - public string PropertyValue { get; set; } - public FilterOperations FilterOperation { get; set; } - - private FilterOperations GetFilterOperation(string prefix) - { - if (prefix.Length == 0) return FilterOperations.eq; - - FilterOperations opertion; - if (!Enum.TryParse(prefix, out opertion)) - throw new JsonApiException("400", $"Invalid filter prefix '{prefix}'"); - - return opertion; - } - - private AttrAttribute GetAttribute(string propertyName) + public class AttrFilterQuery : BaseFilterQuery { - return _jsonApiContext.RequestEntity.Attributes - .FirstOrDefault(attr => - attr.InternalAttributeName.ToLower() == propertyName.ToLower() - ); + private readonly IJsonApiContext _jsonApiContext; + + public AttrFilterQuery( + IJsonApiContext jsonApiCopntext, + FilterQuery filterQuery) + { + _jsonApiContext = jsonApiCopntext; + + var attribute = GetAttribute(filterQuery.Key); + + if (attribute == null) + throw new JsonApiException("400", $"{filterQuery.Key} is not a valid property."); + + FilteredAttribute = attribute; + PropertyValue = filterQuery.Value; + FilterOperation = GetFilterOperation(filterQuery.Operation); + } + + public AttrAttribute FilteredAttribute { get; set; } + public string PropertyValue { get; set; } + public FilterOperations FilterOperation { get; set; } + + private AttrAttribute GetAttribute(string propertyName) + { + return _jsonApiContext.RequestEntity.Attributes + .FirstOrDefault(attr => + attr.InternalAttributeName.ToLower() == propertyName.ToLower() + ); + } } - } } \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs new file mode 100644 index 0000000000..fdb2abd4ae --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs @@ -0,0 +1,18 @@ +using System; + +namespace JsonApiDotNetCore.Internal.Query +{ + public class BaseFilterQuery + { + protected FilterOperations GetFilterOperation(string prefix) + { + if (prefix.Length == 0) return FilterOperations.eq; + + FilterOperations opertion; + if (!Enum.TryParse(prefix, out opertion)) + throw new JsonApiException("400", $"Invalid filter prefix '{prefix}'"); + + return opertion; + } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs index 11ad90281c..7f4f1a40c6 100644 --- a/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs @@ -12,5 +12,6 @@ public FilterQuery(string key, string value, string operation) public string Key { get; set; } public string Value { get; set; } public string Operation { get; set; } + public bool IsAttributeOfRelationship => Key.Contains("."); } } \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs new file mode 100644 index 0000000000..7fb93b8d46 --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs @@ -0,0 +1,51 @@ +using System.Linq; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; + +namespace JsonApiDotNetCore.Internal.Query +{ + public class RelatedAttrFilterQuery : BaseFilterQuery + { + private readonly IJsonApiContext _jsonApiContext; + + public RelatedAttrFilterQuery( + IJsonApiContext jsonApiCopntext, + FilterQuery filterQuery) + { + _jsonApiContext = jsonApiCopntext; + + var relationshipArray = filterQuery.Key.Split('.'); + + var relationship = GetRelationship(relationshipArray[0]); + if (relationship == null) + throw new JsonApiException("400", $"{relationshipArray[0]} is not a valid relationship."); + + var attribute = GetAttribute(relationship, relationshipArray[1]); + if (attribute == null) + throw new JsonApiException("400", $"{relationshipArray[1]} is not a valid attribute on {relationshipArray[0]}."); + + FilteredRelationship = relationship; + FilteredAttribute = attribute; + PropertyValue = filterQuery.Value; + FilterOperation = GetFilterOperation(filterQuery.Operation); + } + + public AttrAttribute FilteredAttribute { get; set; } + public string PropertyValue { get; set; } + public FilterOperations FilterOperation { get; set; } + public RelationshipAttribute FilteredRelationship { get; private set; } + + private RelationshipAttribute GetRelationship(string propertyName) + { + return _jsonApiContext.RequestEntity.Relationships + .FirstOrDefault(r => r.InternalRelationshipName.ToLower() == propertyName.ToLower()); + } + + private AttrAttribute GetAttribute(RelationshipAttribute relationship, string attribute) + { + var relatedContextExntity = _jsonApiContext.ContextGraph.GetContextEntity(relationship.Type); + return relatedContextExntity.Attributes + .FirstOrDefault(a => a.InternalAttributeName.ToLower() == attribute.ToLower()); + } + } +} \ No newline at end of file From 87ec1a6d6a81fcede79bc19801d9249734f6f112 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 11 Apr 2017 07:51:00 -0500 Subject: [PATCH 07/12] feat(IQueryableExt): allow for filtering on attribute of included rel --- .../Extensions/IQueryableExtensions.cs | 125 +++++++++++++----- 1 file changed, 89 insertions(+), 36 deletions(-) diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index 1ffeee4d5a..62313e3ad9 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -86,36 +86,7 @@ public static IQueryable Filter(this IQueryable sourc // {1} var right = Expression.Constant(convertedValue, property.PropertyType); - Expression body; - switch (filterQuery.FilterOperation) - { - case FilterOperations.eq: - // {model.Id == 1} - body = Expression.Equal(left, right); - break; - case FilterOperations.lt: - // {model.Id < 1} - body = Expression.LessThan(left, right); - break; - case FilterOperations.gt: - // {model.Id > 1} - body = Expression.GreaterThan(left, right); - break; - case FilterOperations.le: - // {model.Id <= 1} - body = Expression.LessThanOrEqual(left, right); - break; - case FilterOperations.ge: - // {model.Id <= 1} - body = Expression.GreaterThanOrEqual(left, right); - break; - case FilterOperations.like: - // {model.Id <= 1} - body = Expression.Call(left, "Contains", null, right); - break; - default: - throw new JsonApiException("500", $"Unknown filter operation {filterQuery.FilterOperation}"); - } + var body = GetFilterExpressionLambda(left, right, filterQuery.FilterOperation); var lambda = Expression.Lambda>(body, parameter); @@ -126,27 +97,109 @@ public static IQueryable Filter(this IQueryable sourc throw new JsonApiException("400", $"Could not cast {filterQuery.PropertyValue} to {property.PropertyType.Name}"); } } + + public static IQueryable Filter(this IQueryable source, RelatedAttrFilterQuery filterQuery) + { + if (filterQuery == null) + return source; + + var concreteType = typeof(TSource); + var relation = concreteType.GetProperty(filterQuery.FilteredRelationship.InternalRelationshipName); + if (relation == null) + throw new ArgumentException($"'{filterQuery.FilteredRelationship.InternalRelationshipName}' is not a valid relationship of '{concreteType}'"); + + var relatedType = filterQuery.FilteredRelationship.Type; + var relatedAttr = relatedType.GetProperty(filterQuery.FilteredAttribute.InternalAttributeName); + if (relatedAttr == null) + throw new ArgumentException($"'{filterQuery.FilteredAttribute.InternalAttributeName}' is not a valid attribute of '{filterQuery.FilteredRelationship.InternalRelationshipName}'"); + + try + { + // convert the incoming value to the target value type + // "1" -> 1 + var convertedValue = TypeHelper.ConvertType(filterQuery.PropertyValue, relatedAttr.PropertyType); + // {model} + var parameter = Expression.Parameter(concreteType, "model"); + + // {model.Relationship} + var leftRelationship = Expression.PropertyOrField(parameter, relation.Name); + + // {model.Relationship.Attr} + var left = Expression.PropertyOrField(leftRelationship, relatedAttr.Name); + + // {1} + var right = Expression.Constant(convertedValue, relatedAttr.PropertyType); + + var body = GetFilterExpressionLambda(left, right, filterQuery.FilterOperation); + + var lambda = Expression.Lambda>(body, parameter); + + return source.Where(lambda); + } + catch (FormatException) + { + throw new JsonApiException("400", $"Could not cast {filterQuery.PropertyValue} to {relatedAttr.PropertyType.Name}"); + } + } + + private static Expression GetFilterExpressionLambda(Expression left, Expression right, FilterOperations operation) + { + Expression body; + switch (operation) + { + case FilterOperations.eq: + // {model.Id == 1} + body = Expression.Equal(left, right); + break; + case FilterOperations.lt: + // {model.Id < 1} + body = Expression.LessThan(left, right); + break; + case FilterOperations.gt: + // {model.Id > 1} + body = Expression.GreaterThan(left, right); + break; + case FilterOperations.le: + // {model.Id <= 1} + body = Expression.LessThanOrEqual(left, right); + break; + case FilterOperations.ge: + // {model.Id <= 1} + body = Expression.GreaterThanOrEqual(left, right); + break; + case FilterOperations.like: + // {model.Id <= 1} + body = Expression.Call(left, "Contains", null, right); + break; + default: + throw new JsonApiException("500", $"Unknown filter operation {operation}"); + } + + return body; + } + + public static IQueryable Select(this IQueryable source, IEnumerable columns) { - if(columns == null || columns.Count() == 0) + if (columns == null || columns.Count() == 0) return source; var sourceType = source.ElementType; - + var resultType = typeof(TSource); // {model} var parameter = Expression.Parameter(sourceType, "model"); - + var bindings = columns.Select(column => Expression.Bind( resultType.GetProperty(column), Expression.PropertyOrField(parameter, column))); - + // { new Model () { Property = model.Property } } var body = Expression.MemberInit(Expression.New(resultType), bindings); - + // { model => new TodoItem() { Property = model.Property } } var selector = Expression.Lambda(body, parameter); - + return source.Provider.CreateQuery( Expression.Call(typeof(Queryable), "Select", new Type[] { sourceType, resultType }, source.Expression, Expression.Quote(selector))); From 301bb4382aaa7573086eeaa156aec6556274d50a Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 11 Apr 2017 07:51:36 -0500 Subject: [PATCH 08/12] feat(default-entity-repository): use new filter extension --- src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index aaff9afb7d..f7b0a5e960 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -56,10 +56,10 @@ public virtual IQueryable Filter(IQueryable entities, FilterQ if(filterQuery == null) return entities; - var attributeFilterQuery = new AttrFilterQuery(_jsonApiContext, filterQuery); - - return entities - .Filter(attributeFilterQuery); + if(filterQuery.IsAttributeOfRelationship) + return entities.Filter(new RelatedAttrFilterQuery(_jsonApiContext, filterQuery)); + else + return entities.Filter(new AttrFilterQuery(_jsonApiContext, filterQuery)); } public virtual IQueryable Sort(IQueryable entities, List sortQueries) From 7a86cb4998e03ea2612b2c5cd9546ae524b74ca1 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 11 Apr 2017 07:52:22 -0500 Subject: [PATCH 09/12] test(attr-filter-tests): fix deserialization method --- .../Acceptance/Spec/AttributeFilterTests.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs index 0a0a17d494..3405eab431 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs @@ -13,6 +13,8 @@ using JsonApiDotNetCore.Serialization; using System.Linq; using Person = JsonApiDotNetCoreExample.Models.Person; +using Newtonsoft.Json; +using JsonApiDotNetCore.Models; namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec { @@ -82,7 +84,7 @@ public async Task Can_Filter_On_Related_Attrs() var builder = new WebHostBuilder() .UseStartup(); var httpMethod = new HttpMethod("GET"); - var route = $"/api/v1/todo-items?include[owner]&filter[owner.first-name]={person.FirstName}"; + var route = $"/api/v1/todo-items?include=owner&filter[owner.first-name]={person.FirstName}"; var server = new TestServer(builder); var client = server.CreateClient(); var request = new HttpRequestMessage(httpMethod, route); @@ -90,15 +92,15 @@ public async Task Can_Filter_On_Related_Attrs() // act var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var deserializedBody = _fixture - .GetService() - .DeserializeList(body); + var documents = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + var included = documents.Included; // assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotEmpty(deserializedBody); - foreach(var item in deserializedBody) - Assert.Equal(person.FirstName, item.Owner.FirstName); + Assert.NotNull(included); + Assert.NotEmpty(included); + foreach(var item in included) + Assert.Equal(person.FirstName, item.Attributes["first-name"]); } } } From 5ade61a46f7ec5e62a76da43ed1506466958d202 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 11 Apr 2017 07:54:16 -0500 Subject: [PATCH 10/12] chore(csproj): bump package version --- src/JsonApiDotNetCore/JsonApiDotNetCore.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index b2d2bd4a6c..99a0884eeb 100755 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -1,6 +1,6 @@  - 1.3.1 + 1.3.2 netcoreapp1.0 JsonApiDotNetCore JsonApiDotNetCore From 8a079493946262eb726999e8b97e632636f37117 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 11 Apr 2017 08:30:31 -0500 Subject: [PATCH 11/12] chore(csproj): downgrade package version --- src/JsonApiDotNetCore/JsonApiDotNetCore.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index 99a0884eeb..b2d2bd4a6c 100755 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -1,6 +1,6 @@  - 1.3.2 + 1.3.1 netcoreapp1.0 JsonApiDotNetCore JsonApiDotNetCore From 581f4bd32a1fc0692e703b0d82a344be5a5a86c0 Mon Sep 17 00:00:00 2001 From: Jan Mattner Date: Wed, 12 Apr 2017 14:51:52 +0200 Subject: [PATCH 12/12] fix(DasherizedRoutingConvention): Check for abstract subtype --- .../Controllers/JsonApiControllerMixin.cs | 4 +-- .../Internal/DasherizedRoutingConvention.cs | 7 ++--- .../Controllers/TodoItemsTestController.cs | 29 +++++++++++++++++++ .../Extensibility/CustomControllerTests.cs | 23 +++++++++++++++ 4 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 src/JsonApiDotNetCoreExample/Controllers/TodoItemsTestController.cs diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiControllerMixin.cs b/src/JsonApiDotNetCore/Controllers/JsonApiControllerMixin.cs index b97b4e135a..3200ff36ef 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiControllerMixin.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiControllerMixin.cs @@ -2,9 +2,9 @@ namespace JsonApiDotNetCore.Controllers { - public class JsonApiControllerMixin : Controller + public abstract class JsonApiControllerMixin : Controller { - public JsonApiControllerMixin() + protected JsonApiControllerMixin() { } protected IActionResult UnprocessableEntity() diff --git a/src/JsonApiDotNetCore/Internal/DasherizedRoutingConvention.cs b/src/JsonApiDotNetCore/Internal/DasherizedRoutingConvention.cs index 97be6aee14..9fb23afab4 100644 --- a/src/JsonApiDotNetCore/Internal/DasherizedRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Internal/DasherizedRoutingConvention.cs @@ -29,12 +29,9 @@ public void Apply(ApplicationModel application) } } - private bool IsJsonApiController(ControllerModel controller) + private bool IsJsonApiController(ControllerModel controller) { - var controllerBaseType = controller.ControllerType.BaseType; - if(!controllerBaseType.IsConstructedGenericType) return false; - var genericTypeDefinition = controllerBaseType.GetGenericTypeDefinition(); - return (genericTypeDefinition == typeof(JsonApiController<,>) || genericTypeDefinition == typeof(JsonApiController<>)); + return controller.ControllerType.IsSubclassOf(typeof(JsonApiControllerMixin)); } } } diff --git a/src/JsonApiDotNetCoreExample/Controllers/TodoItemsTestController.cs b/src/JsonApiDotNetCoreExample/Controllers/TodoItemsTestController.cs new file mode 100644 index 0000000000..52a6513f24 --- /dev/null +++ b/src/JsonApiDotNetCoreExample/Controllers/TodoItemsTestController.cs @@ -0,0 +1,29 @@ +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Data; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; +using JsonApiDotNetCoreExample.Models; +using Microsoft.Extensions.Logging; + +namespace JsonApiDotNetCoreExample.Controllers +{ + public abstract class AbstractTodoItemsController : JsonApiController where T : class, IIdentifiable + { + protected AbstractTodoItemsController( + IJsonApiContext jsonApiContext, + IEntityRepository entityRepository, + ILoggerFactory loggerFactory) + : base(jsonApiContext, entityRepository, loggerFactory) + { + } +} + public class TodoItemsTestController : AbstractTodoItemsController + { + public TodoItemsTestController( + IJsonApiContext jsonApiContext, + IEntityRepository entityRepository, + ILoggerFactory loggerFactory) + : base(jsonApiContext, entityRepository, loggerFactory) + { } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs index fe84817540..97043cc8e9 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs @@ -1,10 +1,13 @@ using System.Net; using System.Net.Http; using System.Threading.Tasks; +using DotNetCoreDocs.Models; +using JsonApiDotNetCore.Serialization; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using Xunit; using JsonApiDotNetCoreExample; +using JsonApiDotNetCoreExample.Models; namespace JsonApiDotNetCoreExampleTests.Acceptance.Extensibility { @@ -30,5 +33,25 @@ public async Task NonJsonApiControllers_DoNotUse_Dasherized_Routes() // assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); } + + [Fact] + public async Task InheritedJsonApiControllers_Uses_Dasherized_Routes() + { + // Arrange + var builder = new WebHostBuilder() + .UseStartup(); + var httpMethod = new HttpMethod("GET"); + var route = "/api/v1/todo-items-test"; + + var server = new TestServer(builder); + var client = server.CreateClient(); + var request = new HttpRequestMessage(httpMethod, route); + + // act + var response = await client.SendAsync(request); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } } }