From 72ffb772b4fa6b85836f701618a2571dfd1262ae Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 28 Mar 2017 21:21:31 -0500 Subject: [PATCH 01/10] chore(csproj): bump package version --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1c886507e1..2ae40744a8 100644 --- a/README.md +++ b/README.md @@ -46,14 +46,14 @@ Install-Package JsonApiDotnetCore - project.json ```json -"JsonApiDotNetCore": "1.2.1" +"JsonApiDotNetCore": "1.3.0" ``` - *.csproj ```xml - + ``` From 8a4c9271898d353d88084f6c06e04a36968abfae Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 28 Mar 2017 22:21:06 -0500 Subject: [PATCH 02/10] feat(csproj): add System.ValueTuple --- src/JsonApiDotNetCore/JsonApiDotNetCore.csproj | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index 8872a457b0..45684b0223 100755 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -1,19 +1,17 @@  - - 1.2.2 + 1.3.0 netcoreapp1.0 JsonApiDotNetCore JsonApiDotNetCore 1.1.1 $(PackageTargetFallback);dnxcore50;portable-net45+win8 - + - - + \ No newline at end of file From 0faabbeb0b567a828e2432ef19249d60d7af8240 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 28 Mar 2017 22:22:32 -0500 Subject: [PATCH 03/10] feat(*): add AttrFilterQuery --- .../Extensions/IQueryableExtensions.cs | 2 +- .../Internal/Query/AttrFilterQuery.cs | 51 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index abd1686a22..e33359e90d 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -63,7 +63,7 @@ private static IOrderedQueryable CallGenericOrderMethod(IQuery return (IOrderedQueryable)result; } - public static IQueryable Filter(this IQueryable source, FilterQuery filterQuery) + public static IQueryable Filter(this IQueryable source, AttrFilterQuery filterQuery) { if (filterQuery == null) return source; diff --git a/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs new file mode 100644 index 0000000000..1a691d1d15 --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs @@ -0,0 +1,51 @@ +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) + { + return _jsonApiContext.RequestEntity.Attributes + .FirstOrDefault(attr => + attr.InternalAttributeName.ToLower() == propertyName.ToLower() + ); + } + } +} \ No newline at end of file From c7390ad785e7e51e03e416b584b1b8723f862a7a Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 28 Mar 2017 22:23:03 -0500 Subject: [PATCH 04/10] refactor(FilterQuery): generalize query using string values --- .../Internal/Query/FilterQuery.cs | 16 ++++----- .../Internal/Query/QuerySet.cs | 33 +++++-------------- 2 files changed, 15 insertions(+), 34 deletions(-) diff --git a/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs index 364d0342c6..11ad90281c 100644 --- a/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs @@ -1,18 +1,16 @@ -using JsonApiDotNetCore.Models; - namespace JsonApiDotNetCore.Internal.Query { public class FilterQuery { - public FilterQuery(AttrAttribute filteredAttribute, string propertyValue, FilterOperations filterOperation) + public FilterQuery(string key, string value, string operation) { - FilteredAttribute = filteredAttribute; - PropertyValue = propertyValue; - FilterOperation = filterOperation; + Key = key; + Value = value; + Operation = operation; } - public AttrAttribute FilteredAttribute { get; set; } - public string PropertyValue { get; set; } - public FilterOperations FilterOperation { get; set; } + public string Key { get; set; } + public string Value { get; set; } + public string Operation { get; set; } } } \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs b/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs index 58f1c189f1..80b1870a94 100644 --- a/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs +++ b/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs @@ -74,49 +74,32 @@ private List ParseFilterQuery(string key, string value) var queries = new List(); var propertyName = key.Split('[', ']')[1].ToProperCase(); - var attribute = GetAttribute(propertyName); - - if (attribute == null) - throw new JsonApiException("400", $"{propertyName} is not a valid property."); var values = value.Split(','); foreach(var val in values) - queries.Add(ParseFilterOperation(attribute, val)); + { + (var operation, var filterValue) = ParseFilterOperation(val); + queries.Add(new FilterQuery(propertyName, filterValue, operation)); + } return queries; } - private FilterQuery ParseFilterOperation(AttrAttribute attribute, string value) + private (string operation, string value) ParseFilterOperation(string value) { if(value.Length < 3) - return new FilterQuery(attribute, value, FilterOperations.eq); + return (string.Empty, value); var operation = value.Split(':'); if(operation.Length == 1) - return new FilterQuery(attribute, value, FilterOperations.eq); + return (string.Empty, value); // remove prefix from value var prefix = operation[0]; value = operation[1]; - switch(prefix) - { - case "eq": - return new FilterQuery(attribute, value, FilterOperations.eq); - case "lt": - return new FilterQuery(attribute, value, FilterOperations.lt); - case "gt": - return new FilterQuery(attribute, value, FilterOperations.gt); - case "le": - return new FilterQuery(attribute, value, FilterOperations.le); - case "ge": - return new FilterQuery(attribute, value, FilterOperations.ge); - case "like": - return new FilterQuery(attribute, value, FilterOperations.like); - } - - throw new JsonApiException("400", $"Invalid filter prefix '{prefix}'"); + return (prefix, value);; } private PageQuery ParsePageQuery(string key, string value) From 150aa7c50c344828a7176338ea607d7d1816afbb Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Tue, 28 Mar 2017 22:23:40 -0500 Subject: [PATCH 05/10] feat(default-entity-repository): use the new AttrFilterQuery --- src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 62102bec7b..aaff9afb7d 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -56,8 +56,10 @@ public virtual IQueryable Filter(IQueryable entities, FilterQ if(filterQuery == null) return entities; + var attributeFilterQuery = new AttrFilterQuery(_jsonApiContext, filterQuery); + return entities - .Filter(filterQuery); + .Filter(attributeFilterQuery); } public virtual IQueryable Sort(IQueryable entities, List sortQueries) From 47a73acb50edb0ae3e1d646e567f4cc1b1d4ad76 Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Wed, 29 Mar 2017 21:25:06 -0500 Subject: [PATCH 06/10] test(acceptance): reproduce issue #78 --- .../Data/AppDbContext.cs | 13 +++ .../JsonApiDotNetCoreExample.csproj | 1 + ...330020650_AddAssignedTodoItems.Designer.cs | 100 ++++++++++++++++++ .../20170330020650_AddAssignedTodoItems.cs | 45 ++++++++ .../Migrations/AppDbContextModelSnapshot.cs | 8 ++ src/JsonApiDotNetCoreExample/Models/Person.cs | 3 + .../Models/TodoItem.cs | 4 + .../Acceptance/Spec/DocumentTests/Included.cs | 34 ++++++ 8 files changed, 208 insertions(+) create mode 100755 src/JsonApiDotNetCoreExample/Migrations/20170330020650_AddAssignedTodoItems.Designer.cs create mode 100755 src/JsonApiDotNetCoreExample/Migrations/20170330020650_AddAssignedTodoItems.cs diff --git a/src/JsonApiDotNetCoreExample/Data/AppDbContext.cs b/src/JsonApiDotNetCoreExample/Data/AppDbContext.cs index 7406a11065..592513b94d 100644 --- a/src/JsonApiDotNetCoreExample/Data/AppDbContext.cs +++ b/src/JsonApiDotNetCoreExample/Data/AppDbContext.cs @@ -9,6 +9,19 @@ public AppDbContext(DbContextOptions options) : base(options) { } + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasOne(t => t.Assignee) + .WithMany(p => p.AssignedTodoItems) + .HasForeignKey(t => t.AssigneeId); + + modelBuilder.Entity() + .HasOne(t => t.Owner) + .WithMany(p => p.TodoItems) + .HasForeignKey(t => t.OwnerId); + } + public DbSet TodoItems { get; set; } public DbSet People { get; set; } public DbSet TodoItemCollections { get; set; } diff --git a/src/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj b/src/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj index 60e351ab00..f49c233595 100755 --- a/src/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj +++ b/src/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj @@ -28,6 +28,7 @@ + diff --git a/src/JsonApiDotNetCoreExample/Migrations/20170330020650_AddAssignedTodoItems.Designer.cs b/src/JsonApiDotNetCoreExample/Migrations/20170330020650_AddAssignedTodoItems.Designer.cs new file mode 100755 index 0000000000..52b60adbcb --- /dev/null +++ b/src/JsonApiDotNetCoreExample/Migrations/20170330020650_AddAssignedTodoItems.Designer.cs @@ -0,0 +1,100 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using JsonApiDotNetCoreExample.Data; + +namespace JsonApiDotNetCoreExample.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20170330020650_AddAssignedTodoItems")] + partial class AddAssignedTodoItems + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn) + .HasAnnotation("ProductVersion", "1.1.1"); + + modelBuilder.Entity("JsonApiDotNetCoreExample.Models.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("FirstName"); + + b.Property("LastName"); + + b.HasKey("Id"); + + b.ToTable("People"); + }); + + modelBuilder.Entity("JsonApiDotNetCoreExample.Models.TodoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AssigneeId"); + + b.Property("CollectionId"); + + b.Property("Description"); + + b.Property("Ordinal"); + + b.Property("OwnerId"); + + b.HasKey("Id"); + + b.HasIndex("AssigneeId"); + + b.HasIndex("CollectionId"); + + b.HasIndex("OwnerId"); + + b.ToTable("TodoItems"); + }); + + modelBuilder.Entity("JsonApiDotNetCoreExample.Models.TodoItemCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Name"); + + b.Property("OwnerId"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("TodoItemCollections"); + }); + + modelBuilder.Entity("JsonApiDotNetCoreExample.Models.TodoItem", b => + { + b.HasOne("JsonApiDotNetCoreExample.Models.Person", "Assignee") + .WithMany("AssignedTodoItems") + .HasForeignKey("AssigneeId"); + + b.HasOne("JsonApiDotNetCoreExample.Models.TodoItemCollection", "Collection") + .WithMany("TodoItems") + .HasForeignKey("CollectionId"); + + b.HasOne("JsonApiDotNetCoreExample.Models.Person", "Owner") + .WithMany("TodoItems") + .HasForeignKey("OwnerId"); + }); + + modelBuilder.Entity("JsonApiDotNetCoreExample.Models.TodoItemCollection", b => + { + b.HasOne("JsonApiDotNetCoreExample.Models.Person", "Owner") + .WithMany("TodoItemCollections") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/src/JsonApiDotNetCoreExample/Migrations/20170330020650_AddAssignedTodoItems.cs b/src/JsonApiDotNetCoreExample/Migrations/20170330020650_AddAssignedTodoItems.cs new file mode 100755 index 0000000000..9d41bb041e --- /dev/null +++ b/src/JsonApiDotNetCoreExample/Migrations/20170330020650_AddAssignedTodoItems.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace JsonApiDotNetCoreExample.Migrations +{ + public partial class AddAssignedTodoItems : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AssigneeId", + table: "TodoItems", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_TodoItems_AssigneeId", + table: "TodoItems", + column: "AssigneeId"); + + migrationBuilder.AddForeignKey( + name: "FK_TodoItems_People_AssigneeId", + table: "TodoItems", + column: "AssigneeId", + principalTable: "People", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_TodoItems_People_AssigneeId", + table: "TodoItems"); + + migrationBuilder.DropIndex( + name: "IX_TodoItems_AssigneeId", + table: "TodoItems"); + + migrationBuilder.DropColumn( + name: "AssigneeId", + table: "TodoItems"); + } + } +} diff --git a/src/JsonApiDotNetCoreExample/Migrations/AppDbContextModelSnapshot.cs b/src/JsonApiDotNetCoreExample/Migrations/AppDbContextModelSnapshot.cs index 58722dd4f7..21f5cbc221 100755 --- a/src/JsonApiDotNetCoreExample/Migrations/AppDbContextModelSnapshot.cs +++ b/src/JsonApiDotNetCoreExample/Migrations/AppDbContextModelSnapshot.cs @@ -35,6 +35,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Id") .ValueGeneratedOnAdd(); + b.Property("AssigneeId"); + b.Property("CollectionId"); b.Property("Description"); @@ -45,6 +47,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("AssigneeId"); + b.HasIndex("CollectionId"); b.HasIndex("OwnerId"); @@ -70,6 +74,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("JsonApiDotNetCoreExample.Models.TodoItem", b => { + b.HasOne("JsonApiDotNetCoreExample.Models.Person", "Assignee") + .WithMany("AssignedTodoItems") + .HasForeignKey("AssigneeId"); + b.HasOne("JsonApiDotNetCoreExample.Models.TodoItemCollection", "Collection") .WithMany("TodoItems") .HasForeignKey("CollectionId"); diff --git a/src/JsonApiDotNetCoreExample/Models/Person.cs b/src/JsonApiDotNetCoreExample/Models/Person.cs index 3689f39537..52b67347e9 100644 --- a/src/JsonApiDotNetCoreExample/Models/Person.cs +++ b/src/JsonApiDotNetCoreExample/Models/Person.cs @@ -15,6 +15,9 @@ public class Person : Identifiable, IHasMeta [HasMany("todo-items")] public virtual List TodoItems { get; set; } + + [HasMany("assigned-todo-items")] + public virtual List AssignedTodoItems { get; set; } [HasMany("todo-item-collections")] public virtual List TodoItemCollections { get; set; } diff --git a/src/JsonApiDotNetCoreExample/Models/TodoItem.cs b/src/JsonApiDotNetCoreExample/Models/TodoItem.cs index 1c27c043d6..008c42b1a6 100644 --- a/src/JsonApiDotNetCoreExample/Models/TodoItem.cs +++ b/src/JsonApiDotNetCoreExample/Models/TodoItem.cs @@ -12,11 +12,15 @@ public class TodoItem : Identifiable public long Ordinal { get; set; } public int? OwnerId { get; set; } + public int? AssigneeId { get; set; } public Guid? CollectionId { get; set; } [HasOne("owner")] public virtual Person Owner { get; set; } + [HasOne("assignee")] + public virtual Person Assignee { get; set; } + [HasOne("collection")] public virtual TodoItemCollection Collection { get; set; } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs index 9ddf5519cf..39747fd68e 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Included.cs @@ -134,6 +134,40 @@ public async Task GET_Included_Contains_SideloadedData_OneToMany() Assert.Equal(documents.Data.Count, documents.Included.Count); } + [Fact] + public async Task GET_Included_DoesNot_Duplicate_Records_ForMultipleRelationshipsOfSameType() + { + // arrange + _context.People.RemoveRange(_context.People); // ensure all people have todo-items + _context.TodoItems.RemoveRange(_context.TodoItems); + var person = _personFaker.Generate(); + var todoItem = _todoItemFaker.Generate(); + todoItem.Owner = person; + todoItem.Assignee = person; + _context.TodoItems.Add(todoItem); + _context.SaveChanges(); + + var builder = new WebHostBuilder() + .UseStartup(); + + var httpMethod = new HttpMethod("GET"); + var route = $"/api/v1/todo-items/{todoItem.Id}?include=owner&include=assignee"; + + var server = new TestServer(builder); + var client = server.CreateClient(); + var request = new HttpRequestMessage(httpMethod, route); + + // act + var response = await client.SendAsync(request); + var documents = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + var data = documents.Data; + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotEmpty(documents.Included); + Assert.Equal(1, documents.Included.Count); + } + [Fact] public async Task GET_ById_Included_Contains_SideloadedData_ForOneToMany() { From 0ab51ef6f7e69b6da26085387805acc6e5f8efca Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Wed, 29 Mar 2017 21:26:07 -0500 Subject: [PATCH 07/10] fix(document-builder): unique inclusion of entity ensures that an entity will only be added to the included references once --- src/JsonApiDotNetCore/Builders/DocumentBuilder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index 92c785c5d0..971fd2af10 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -188,7 +188,8 @@ private List GetIncludedEntities(ContextEntity contextEntity, IIde private void AddIncludedEntity(List entities, IIdentifiable entity) { var includedEntity = GetIncludedEntity(entity); - if(includedEntity != null) + + if(includedEntity != null && !entities.Any(doc => doc.Id == includedEntity.Id && doc.Type == includedEntity.Type)) entities.Add(includedEntity); } From c01e4c83d06633dbbfb20c525973bc2b77791f90 Mon Sep 17 00:00:00 2001 From: Jared Nance Date: Wed, 29 Mar 2017 21:36:03 -0500 Subject: [PATCH 08/10] docs(readme): demonstrate custom filter usage --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index 2ae40744a8..362225c453 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ JsonApiDotnetCore provides a framework for building [json:api](http://jsonapi.or - [Defining Custom Data Access Methods](#defining-custom-data-access-methods) - [Pagination](#pagination) - [Filtering](#filtering) + - [Custom Filters](#custom-filters) - [Sorting](#sorting) - [Meta](#meta) - [Client Generated Ids](#client-generated-ids) @@ -317,6 +318,31 @@ identifier): ?filter[attribute]=like:value ``` +#### Custom Filters + +You can customize the filter implementation by overriding the method in the `DefaultEntityRepository` like so: + +```csharp +public class MyEntityRepository : DefaultEntityRepository +{ + public MyEntityRepository( + AppDbContext context, + ILoggerFactory loggerFactory, + IJsonApiContext jsonApiContext) + : base(context, loggerFactory, jsonApiContext) + { } + + public override IQueryable Filter(IQueryable entities, FilterQuery filterQuery) + { + // use the base filtering method + entities = base.Filter(entities, filterQuery); + + // implement custom method + return ApplyMyCustomFilter(entities, filterQuery); + } +} +``` + ### Sorting Resources can be sorted by an attribute: From cbe3178463dfa4ac534b8ca80e739f527f82134c Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Thu, 30 Mar 2017 18:55:44 -0500 Subject: [PATCH 09/10] test(acceptance): reproduce issue --- ...20170330234539_AddGuidProperty.Designer.cs | 102 ++++++++++++++++++ .../20170330234539_AddGuidProperty.cs | 25 +++++ .../Migrations/AppDbContextModelSnapshot.cs | 2 + .../Models/TodoItem.cs | 8 ++ .../Acceptance/Spec/AttributeFilterTests.cs | 67 ++++++++++++ .../Spec/FetchingRelationshipsTests.cs | 7 -- 6 files changed, 204 insertions(+), 7 deletions(-) create mode 100755 src/JsonApiDotNetCoreExample/Migrations/20170330234539_AddGuidProperty.Designer.cs create mode 100755 src/JsonApiDotNetCoreExample/Migrations/20170330234539_AddGuidProperty.cs create mode 100644 test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs diff --git a/src/JsonApiDotNetCoreExample/Migrations/20170330234539_AddGuidProperty.Designer.cs b/src/JsonApiDotNetCoreExample/Migrations/20170330234539_AddGuidProperty.Designer.cs new file mode 100755 index 0000000000..ded5d1b160 --- /dev/null +++ b/src/JsonApiDotNetCoreExample/Migrations/20170330234539_AddGuidProperty.Designer.cs @@ -0,0 +1,102 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using JsonApiDotNetCoreExample.Data; + +namespace JsonApiDotNetCoreExample.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20170330234539_AddGuidProperty")] + partial class AddGuidProperty + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn) + .HasAnnotation("ProductVersion", "1.1.1"); + + modelBuilder.Entity("JsonApiDotNetCoreExample.Models.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("FirstName"); + + b.Property("LastName"); + + b.HasKey("Id"); + + b.ToTable("People"); + }); + + modelBuilder.Entity("JsonApiDotNetCoreExample.Models.TodoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AssigneeId"); + + b.Property("CollectionId"); + + b.Property("Description"); + + b.Property("GuidProperty"); + + b.Property("Ordinal"); + + b.Property("OwnerId"); + + b.HasKey("Id"); + + b.HasIndex("AssigneeId"); + + b.HasIndex("CollectionId"); + + b.HasIndex("OwnerId"); + + b.ToTable("TodoItems"); + }); + + modelBuilder.Entity("JsonApiDotNetCoreExample.Models.TodoItemCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Name"); + + b.Property("OwnerId"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("TodoItemCollections"); + }); + + modelBuilder.Entity("JsonApiDotNetCoreExample.Models.TodoItem", b => + { + b.HasOne("JsonApiDotNetCoreExample.Models.Person", "Assignee") + .WithMany("AssignedTodoItems") + .HasForeignKey("AssigneeId"); + + b.HasOne("JsonApiDotNetCoreExample.Models.TodoItemCollection", "Collection") + .WithMany("TodoItems") + .HasForeignKey("CollectionId"); + + b.HasOne("JsonApiDotNetCoreExample.Models.Person", "Owner") + .WithMany("TodoItems") + .HasForeignKey("OwnerId"); + }); + + modelBuilder.Entity("JsonApiDotNetCoreExample.Models.TodoItemCollection", b => + { + b.HasOne("JsonApiDotNetCoreExample.Models.Person", "Owner") + .WithMany("TodoItemCollections") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/src/JsonApiDotNetCoreExample/Migrations/20170330234539_AddGuidProperty.cs b/src/JsonApiDotNetCoreExample/Migrations/20170330234539_AddGuidProperty.cs new file mode 100755 index 0000000000..43ea19b242 --- /dev/null +++ b/src/JsonApiDotNetCoreExample/Migrations/20170330234539_AddGuidProperty.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace JsonApiDotNetCoreExample.Migrations +{ + public partial class AddGuidProperty : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "GuidProperty", + table: "TodoItems", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "GuidProperty", + table: "TodoItems"); + } + } +} diff --git a/src/JsonApiDotNetCoreExample/Migrations/AppDbContextModelSnapshot.cs b/src/JsonApiDotNetCoreExample/Migrations/AppDbContextModelSnapshot.cs index 21f5cbc221..6912fa093a 100755 --- a/src/JsonApiDotNetCoreExample/Migrations/AppDbContextModelSnapshot.cs +++ b/src/JsonApiDotNetCoreExample/Migrations/AppDbContextModelSnapshot.cs @@ -41,6 +41,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Description"); + b.Property("GuidProperty"); + b.Property("Ordinal"); b.Property("OwnerId"); diff --git a/src/JsonApiDotNetCoreExample/Models/TodoItem.cs b/src/JsonApiDotNetCoreExample/Models/TodoItem.cs index 008c42b1a6..27ad9716c6 100644 --- a/src/JsonApiDotNetCoreExample/Models/TodoItem.cs +++ b/src/JsonApiDotNetCoreExample/Models/TodoItem.cs @@ -5,11 +5,19 @@ namespace JsonApiDotNetCoreExample.Models { public class TodoItem : Identifiable { + public TodoItem() + { + GuidProperty = Guid.NewGuid(); + } + [Attr("description")] public string Description { get; set; } [Attr("ordinal")] public long Ordinal { get; set; } + + [Attr("guid-property")] + public Guid GuidProperty { get; set; } public int? OwnerId { get; set; } public int? AssigneeId { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs new file mode 100644 index 0000000000..5c10196d1f --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs @@ -0,0 +1,67 @@ +using System.Net; +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; + +namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec +{ + [Collection("WebHostCollection")] + public class AttributeFilterTests + { + private DocsFixture _fixture; + private Faker _todoItemFaker; + + 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()); + } + + [Fact] + public async Task Can_Filter_On_Guid_Properties() + { + // arrange + var context = _fixture.GetService(); + var todoItem = _todoItemFaker.Generate(); + context.TodoItems.Add(todoItem); + await context.SaveChangesAsync(); + + var builder = new WebHostBuilder() + .UseStartup(); + var httpMethod = new HttpMethod("GET"); + var route = $"/api/v1/todo-items?filter[guid-property]={todoItem.GuidProperty}"; + 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); + + var todoItemResponse = deserializedBody.Single(); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(todoItem.Id, todoItemResponse.Id); + Assert.Equal(todoItem.GuidProperty, todoItemResponse.GuidProperty); + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs index 6522e79b62..e6ca2663c0 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingRelationshipsTests.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; @@ -7,18 +5,13 @@ using Bogus; using DotNetCoreDocs; using DotNetCoreDocs.Writers; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; -using Newtonsoft.Json; using Xunit; -using Person = JsonApiDotNetCoreExample.Models.Person; - namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec { [Collection("WebHostCollection")] From 1513211a517b50a7a686fe49d3dbd3bf2fc429de Mon Sep 17 00:00:00 2001 From: jaredcnance Date: Thu, 30 Mar 2017 18:56:14 -0500 Subject: [PATCH 10/10] fix(iQueryableExtensions): use TypeHelper better type conversion for Guids --- src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index e33359e90d..1ffeee4d5a 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -78,7 +78,7 @@ public static IQueryable Filter(this IQueryable sourc { // convert the incoming value to the target value type // "1" -> 1 - var convertedValue = Convert.ChangeType(filterQuery.PropertyValue, property.PropertyType); + var convertedValue = TypeHelper.ConvertType(filterQuery.PropertyValue, property.PropertyType); // {model} var parameter = Expression.Parameter(concreteType, "model"); // {model.Id}