Skip to content

Commit 7a737aa

Browse files
committed
fix: merge
2 parents 446dad3 + c4ebf4b commit 7a737aa

File tree

12 files changed

+139
-35
lines changed

12 files changed

+139
-35
lines changed

docs/usage/resources/resource-definitions.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,9 @@ public class ItemResource : ResourceDefinition<Item>
113113
// handles queries like: ?filter[was-active-on]=2018-10-15T01:25:52Z
114114
public override QueryFilters GetQueryFilters()
115115
=> new QueryFilters {
116-
{ "was-active-on", (items, value) => DateTime.TryParse(value, out dateValue)
116+
{ "was-active-on", (items, filter) => DateTime.TryParse(filter.Value, out dateValue)
117117
? items.Where(i => i.Expired == null || dateValue < i.Expired)
118-
: throw new JsonApiException(400, $"'{value}' is not a valid date.")
118+
: throw new JsonApiException(400, $"'{filter.Value}' is not a valid date.")
119119
}
120120
};
121121
}
@@ -128,4 +128,4 @@ Prior to the introduction of auto-discovery, you needed to register the
128128

129129
```c#
130130
services.AddScoped<ResourceDefinition<Item>, ItemResource>();
131-
```
131+
```

src/Examples/JsonApiDotNetCoreExample/Models/TodoItem.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,13 @@ public TodoItem()
2828
[Attr("achieved-date", isFilterable: false, isSortable: false)]
2929
public DateTime? AchievedDate { get; set; }
3030

31-
3231
[Attr("updated-date")]
3332
public DateTime? UpdatedDate { get; set; }
3433

34+
35+
[Attr("offset-date")]
36+
public DateTimeOffset? OffsetDate { get; set; }
37+
3538
public int? OwnerId { get; set; }
3639
public int? AssigneeId { get; set; }
3740
public Guid? CollectionId { get; set; }
Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,33 @@
11
using System.Collections.Generic;
2+
using System.Linq;
23
using JsonApiDotNetCore.Models;
34
using JsonApiDotNetCoreExample.Models;
5+
using JsonApiDotNetCore.Internal.Query;
46

57
namespace JsonApiDotNetCoreExample.Resources
68
{
79
public class UserResource : ResourceDefinition<User>
810
{
911
protected override List<AttrAttribute> OutputAttrs()
1012
=> Remove(user => user.Password);
11-
13+
14+
public override QueryFilters GetQueryFilters()
15+
{
16+
return new QueryFilters
17+
{
18+
{ "first-character", (users, queryFilter) => FirstCharacterFilter(users, queryFilter) }
19+
};
20+
}
21+
22+
private IQueryable<User> FirstCharacterFilter(IQueryable<User> users, FilterQuery filterQuery)
23+
{
24+
switch(filterQuery.Operation)
25+
{
26+
case "lt":
27+
return users.Where(u => u.Username[0] < filterQuery.Value[0]);
28+
default:
29+
return users.Where(u => u.Username[0] == filterQuery.Value[0]);
30+
}
31+
}
1232
}
1333
}

src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -99,19 +99,8 @@ public DefaultEntityRepository(
9999
_resourceDefinition = resourceDefinition;
100100
}
101101

102-
103-
104-
public virtual IQueryable<TEntity> Get()
105-
{
106-
var entities = (IQueryable<TEntity>)_dbSet;
107-
if (_jsonApiContext.QuerySet?.Fields != null && _jsonApiContext.QuerySet.Fields.Count > 0)
108-
return entities.Select(_jsonApiContext.QuerySet?.Fields);
109-
110-
return entities;
111-
}
112-
113102
/// <inheritdoc />
114-
public virtual IQueryable<TEntity> GetQueryable()
103+
public virtual IQueryable<TEntity> Get()
115104
=> _dbSet;
116105

117106
public virtual IQueryable<TEntity> Select(IQueryable<TEntity> entities, List<string> fields)
@@ -130,7 +119,7 @@ public virtual IQueryable<TEntity> Filter(IQueryable<TEntity> entities, FilterQu
130119
var defaultQueryFilters = _resourceDefinition.GetQueryFilters();
131120
if (defaultQueryFilters != null && defaultQueryFilters.TryGetValue(filterQuery.Attribute, out var defaultQueryFilter) == true)
132121
{
133-
return defaultQueryFilter(entities, filterQuery.Value);
122+
return defaultQueryFilter(entities, filterQuery);
134123
}
135124
}
136125
return entities.Filter(_jsonApiContext, filterQuery);
@@ -161,15 +150,15 @@ public virtual IQueryable<TEntity> Sort(IQueryable<TEntity> entities, List<SortQ
161150
/// <inheritdoc />
162151
public virtual async Task<TEntity> GetAsync(TId id)
163152
{
164-
return await GetQueryable().SingleOrDefaultAsync(e => e.Id.Equals(id));
153+
return await Select(Get(), _jsonApiContext.QuerySet?.Fields).SingleOrDefaultAsync(e => e.Id.Equals(id));
165154
}
166155

167156
/// <inheritdoc />
168157
public virtual async Task<TEntity> GetAndIncludeAsync(TId id, string relationshipName)
169158
{
170159
_logger?.LogDebug($"[JADN] GetAndIncludeAsync({id}, {relationshipName})");
171160

172-
var includedSet = Include(GetQueryable(), relationshipName);
161+
var includedSet = Include(Select(Get(), _jsonApiContext.QuerySet?.Fields), relationshipName);
173162
var result = await includedSet.SingleOrDefaultAsync(e => e.Id.Equals(id));
174163

175164
return result;

src/JsonApiDotNetCore/Data/IEntityReadRepository.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,6 @@ public interface IEntityReadRepository<TEntity, in TId>
2424
/// The base GET query. This is a good place to apply rules that should affect all reads,
2525
/// such as authorization of resources.
2626
/// </summary>
27-
IQueryable<TEntity> GetQueryable();
28-
29-
[Obsolete("This method has been deprecated, use GetQueryable() instead")]
3027
IQueryable<TEntity> Get();
3128

3229
/// <summary>

src/JsonApiDotNetCore/JsonApiDotNetCore.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<VersionPrefix>3.1.0</VersionPrefix>
3+
<VersionPrefix>4.0.0</VersionPrefix>
44
<TargetFrameworks>$(NetStandardVersion)</TargetFrameworks>
55
<AssemblyName>JsonApiDotNetCore</AssemblyName>
66
<PackageId>JsonApiDotNetCore</PackageId>

src/JsonApiDotNetCore/Models/ResourceDefinition.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ public virtual void BeforeImplicitUpdateRelationship(IAffectedRelationships<T> r
197197
/// method signature.
198198
/// See <see cref="GetQueryFilters" /> for usage details.
199199
/// </summary>
200-
public class QueryFilters : Dictionary<string, Func<IQueryable<T>, string, IQueryable<T>>> { }
200+
public class QueryFilters : Dictionary<string, Func<IQueryable<T>, FilterQuery, IQueryable<T>>> { }
201201

202202
/// <summary>
203203
/// Define a the default sort order if no sort key is provided.

src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.IO;
34
using System.Linq;
45
using System.Reflection;
56
using JsonApiDotNetCore.Extensions;
@@ -36,8 +37,12 @@ public object Deserialize(string requestBody)
3637
{
3738
try
3839
{
39-
var bodyJToken = JToken.Parse(requestBody);
40-
40+
JToken bodyJToken;
41+
using (JsonReader jsonReader = new JsonTextReader(new StringReader(requestBody)))
42+
{
43+
jsonReader.DateParseHandling = DateParseHandling.None;
44+
bodyJToken = JToken.Load(jsonReader);
45+
}
4146
if (RequestIsOperation(bodyJToken))
4247
{
4348
_jsonApiContext.IsBulkOperationRequest = true;

src/JsonApiDotNetCore/Services/EntityResourceService.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,15 +114,17 @@ public virtual async Task<bool> DeleteAsync(TId id)
114114
public virtual async Task<IEnumerable<TResource>> GetAsync()
115115
{
116116
_hookExecutor?.BeforeRead<TEntity>(ResourcePipeline.Read);
117-
var entities = _entities.GetQueryable();
117+
var entities = _entities.Get();
118118

119119
entities = ApplySortAndFilterQuery(entities);
120120

121121
if (ShouldIncludeRelationships())
122122
entities = IncludeRelationships(entities, _jsonApiContext.QuerySet.IncludedRelationships);
123+
124+
if (_jsonApiContext.Options.IncludeTotalRecordCount)
125+
_jsonApiContext.PageManager.TotalRecords = await _entities.CountAsync(entities);
123126

124-
if (_jsonApiContext.QuerySet?.Fields?.Count > 0)
125-
entities = _entities.Select(entities, _jsonApiContext.QuerySet.Fields);
127+
entities = _entities.Select(entities, _jsonApiContext.QuerySet?.Fields);
126128

127129
if (!IsNull(_hookExecutor, entities))
128130
{
@@ -298,7 +300,7 @@ protected virtual IQueryable<TEntity> IncludeRelationships(IQueryable<TEntity> e
298300

299301
private async Task<TEntity> GetWithRelationshipsAsync(TId id)
300302
{
301-
var query = _entities.GetQueryable().Where(e => e.Id.Equals(id));
303+
var query = _entities.Select(_entities.Get(), _jsonApiContext.QuerySet?.Fields).Where(e => e.Id.Equals(id));
302304

303305
_jsonApiContext.QuerySet.IncludedRelationships.ForEach(r =>
304306
{
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Net;
4+
using System.Net.Http;
5+
using System.Net.Http.Headers;
6+
using System.Threading.Tasks;
7+
using Bogus;
8+
using JsonApiDotNetCore.Models;
9+
using JsonApiDotNetCore.Serialization;
10+
using JsonApiDotNetCoreExample.Data;
11+
using JsonApiDotNetCoreExample.Models;
12+
using Microsoft.EntityFrameworkCore;
13+
using Newtonsoft.Json;
14+
using Xunit;
15+
16+
namespace JsonApiDotNetCoreExampleTests.Acceptance
17+
{
18+
[Collection("WebHostCollection")]
19+
public class QueryFiltersTests
20+
{
21+
private TestFixture<TestStartup> _fixture;
22+
private AppDbContext _context;
23+
private Faker<User> _userFaker;
24+
25+
public QueryFiltersTests(TestFixture<TestStartup> fixture)
26+
{
27+
_fixture = fixture;
28+
_context = fixture.GetService<AppDbContext>();
29+
_userFaker = new Faker<User>()
30+
.RuleFor(u => u.Username, f => f.Internet.UserName())
31+
.RuleFor(u => u.Password, f => f.Internet.Password());
32+
}
33+
34+
[Fact]
35+
public async Task FiltersWithCustomQueryFiltersEquals()
36+
{
37+
// Arrange
38+
var user = _userFaker.Generate();
39+
var firstUsernameCharacter = user.Username[0];
40+
_context.Users.Add(user);
41+
_context.SaveChanges();
42+
43+
var httpMethod = new HttpMethod("GET");
44+
var route = $"/api/v1/users?filter[first-character]=eq:{firstUsernameCharacter}";
45+
var request = new HttpRequestMessage(httpMethod, route);
46+
47+
// Act
48+
var response = await _fixture.Client.SendAsync(request);
49+
50+
// Assert
51+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
52+
var body = await response.Content.ReadAsStringAsync();
53+
var deserializedBody = _fixture.GetService<IJsonApiDeSerializer>().DeserializeList<User>(body);
54+
var usersWithFirstCharacter = _context.Users.Where(u => u.Username[0] == firstUsernameCharacter);
55+
Assert.True(deserializedBody.All(u => u.Username[0] == firstUsernameCharacter));
56+
}
57+
58+
[Fact]
59+
public async Task FiltersWithCustomQueryFiltersLessThan()
60+
{
61+
// Arrange
62+
var aUser = _userFaker.Generate();
63+
aUser.Username = "alfred";
64+
var zUser = _userFaker.Generate();
65+
zUser.Username = "zac";
66+
_context.Users.AddRange(aUser, zUser);
67+
_context.SaveChanges();
68+
69+
var median = 'h';
70+
71+
var httpMethod = new HttpMethod("GET");
72+
var route = $"/api/v1/users?filter[first-character]=lt:{median}";
73+
var request = new HttpRequestMessage(httpMethod, route);
74+
75+
// Act
76+
var response = await _fixture.Client.SendAsync(request);
77+
78+
// Assert
79+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
80+
var body = await response.Content.ReadAsStringAsync();
81+
var deserializedBody = _fixture.GetService<IJsonApiDeSerializer>().DeserializeList<User>(body);
82+
Assert.True(deserializedBody.All(u => u.Username[0] < median));
83+
}
84+
}
85+
}

0 commit comments

Comments
 (0)