diff --git a/README.md b/README.md
index 791c0ee7d6..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)
@@ -46,14 +47,14 @@ Install-Package JsonApiDotnetCore
- project.json
```json
-"JsonApiDotNetCore": "1.2.2"
+"JsonApiDotNetCore": "1.3.0"
```
- *.csproj
```xml
-
+
```
@@ -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:
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);
}
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)
diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs
index abd1686a22..1ffeee4d5a 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;
@@ -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}
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
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)
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
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/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 58722dd4f7..6912fa093a 100755
--- a/src/JsonApiDotNetCoreExample/Migrations/AppDbContextModelSnapshot.cs
+++ b/src/JsonApiDotNetCoreExample/Migrations/AppDbContextModelSnapshot.cs
@@ -35,16 +35,22 @@ protected override void BuildModel(ModelBuilder modelBuilder)
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");
@@ -70,6 +76,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..27ad9716c6 100644
--- a/src/JsonApiDotNetCoreExample/Models/TodoItem.cs
+++ b/src/JsonApiDotNetCoreExample/Models/TodoItem.cs
@@ -5,18 +5,30 @@ 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; }
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/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/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()
{
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")]