Skip to content

Commit 4eec35a

Browse files
Maurits MoeysMaurits Moeys
Maurits Moeys
authored and
Maurits Moeys
committed
feat: introduced JsonApiActionFilter
1 parent 857b8a9 commit 4eec35a

File tree

4 files changed

+146
-97
lines changed

4 files changed

+146
-97
lines changed

src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ private static void AddMvcOptions(MvcOptions options, JsonApiOptions config)
7979
{
8080
options.Filters.Add(typeof(JsonApiExceptionFilter));
8181
options.Filters.Add(typeof(TypeMatchFilter));
82+
options.Filters.Add(typeof(JsonApiActionFilter));
8283
options.SerializeAsJsonApi(config);
8384

8485
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
using System;
2+
using System.Linq;
3+
using JsonApiDotNetCore.Configuration;
4+
using JsonApiDotNetCore.Internal;
5+
using JsonApiDotNetCore.Internal.Contracts;
6+
using JsonApiDotNetCore.Managers.Contracts;
7+
using JsonApiDotNetCore.Services;
8+
using Microsoft.AspNetCore.Http;
9+
using Microsoft.AspNetCore.Mvc.Filters;
10+
using Microsoft.AspNetCore.Routing;
11+
12+
namespace JsonApiDotNetCore.Middleware
13+
{
14+
public class JsonApiActionFilter : IActionFilter
15+
{
16+
private readonly IJsonApiContext _jsonApiContext;
17+
private readonly IResourceGraph _resourceGraph;
18+
private readonly IRequestManager _requestManager;
19+
private readonly IPageManager _pageManager;
20+
private readonly IQueryParser _queryParser;
21+
private readonly IJsonApiOptions _options;
22+
private HttpContext _httpContext;
23+
public JsonApiActionFilter(IResourceGraph resourceGraph,
24+
IRequestManager requestManager,
25+
IPageManager pageManager,
26+
IQueryParser queryParser,
27+
IJsonApiOptions options)
28+
{
29+
_resourceGraph = resourceGraph;
30+
_requestManager = requestManager;
31+
_pageManager = pageManager;
32+
_queryParser = queryParser;
33+
_options = options;
34+
}
35+
36+
/// <summary>
37+
/// </summary>
38+
public void OnActionExecuting(ActionExecutingContext context)
39+
{
40+
_httpContext = context.HttpContext;
41+
ContextEntity contextEntityCurrent = GetCurrentEntity();
42+
43+
// the contextEntity is null eg when we're using a non-JsonApiDotNetCore route.
44+
if (contextEntityCurrent != null)
45+
{
46+
_requestManager.SetContextEntity(contextEntityCurrent);
47+
_requestManager.BasePath = GetBasePath(contextEntityCurrent.EntityName);
48+
HandleUriParameters();
49+
_requestManager.IsRelationshipPath = PathIsRelationship();
50+
}
51+
52+
}
53+
54+
55+
/// <summary>
56+
/// Parses the uri
57+
/// </summary>
58+
protected void HandleUriParameters()
59+
{
60+
if (_httpContext.Request.Query.Count > 0)
61+
{
62+
var querySet = _queryParser.Parse(_httpContext.Request.Query);
63+
_requestManager.QuerySet = querySet; //this shouldn't be exposed?
64+
_pageManager.PageSize = querySet.PageQuery.PageSize ?? _pageManager.PageSize;
65+
_pageManager.CurrentPage = querySet.PageQuery.PageOffset ?? _pageManager.CurrentPage;
66+
_requestManager.IncludedRelationships = _requestManager.QuerySet.IncludedRelationships;
67+
}
68+
}
69+
70+
protected bool PathIsRelationship()
71+
{
72+
var actionName = (string)_httpContext.GetRouteData().Values["action"];
73+
return actionName.ToLower().Contains("relationships");
74+
}
75+
private string GetBasePath(string entityName)
76+
{
77+
var r = _httpContext.Request;
78+
if (_options.RelativeLinks)
79+
{
80+
return GetNamespaceFromPath(r.Path, entityName);
81+
}
82+
else
83+
{
84+
return $"{r.Scheme}://{r.Host}{GetNamespaceFromPath(r.Path, entityName)}";
85+
}
86+
}
87+
internal static string GetNamespaceFromPath(string path, string entityName)
88+
{
89+
var entityNameSpan = entityName.AsSpan();
90+
var pathSpan = path.AsSpan();
91+
const char delimiter = '/';
92+
for (var i = 0; i < pathSpan.Length; i++)
93+
{
94+
if (pathSpan[i].Equals(delimiter))
95+
{
96+
var nextPosition = i + 1;
97+
if (pathSpan.Length > i + entityNameSpan.Length)
98+
{
99+
var possiblePathSegment = pathSpan.Slice(nextPosition, entityNameSpan.Length);
100+
if (entityNameSpan.SequenceEqual(possiblePathSegment))
101+
{
102+
// check to see if it's the last position in the string
103+
// or if the next character is a /
104+
var lastCharacterPosition = nextPosition + entityNameSpan.Length;
105+
106+
if (lastCharacterPosition == pathSpan.Length || pathSpan.Length >= lastCharacterPosition + 2 && pathSpan[lastCharacterPosition].Equals(delimiter))
107+
{
108+
return pathSpan.Slice(0, i).ToString();
109+
}
110+
}
111+
}
112+
}
113+
}
114+
115+
return string.Empty;
116+
}
117+
/// <summary>
118+
/// Gets the current entity that we need for serialization and deserialization.
119+
/// </summary>
120+
/// <param name="context"></param>
121+
/// <param name="resourceGraph"></param>
122+
/// <returns></returns>
123+
private ContextEntity GetCurrentEntity()
124+
{
125+
var controllerName = (string)_httpContext.GetRouteData().Values["controller"];
126+
return _resourceGraph.GetEntityFromControllerName(controllerName);
127+
}
128+
129+
130+
private bool IsJsonApiRequest(HttpRequest request)
131+
{
132+
return (request.ContentType?.Equals(Constants.ContentType, StringComparison.OrdinalIgnoreCase) == true);
133+
}
134+
135+
public void OnActionExecuted(ActionExecutedContext context) { /* noop */ }
136+
}
137+
}

src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs

Lines changed: 4 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using JsonApiDotNetCore.Configuration;
66
using JsonApiDotNetCore.Internal;
77
using JsonApiDotNetCore.Internal.Contracts;
8+
using JsonApiDotNetCore.Internal.Query;
89
using JsonApiDotNetCore.Managers.Contracts;
910
using JsonApiDotNetCore.Services;
1011
using Microsoft.AspNetCore.Http;
@@ -16,14 +17,13 @@ namespace JsonApiDotNetCore.Middleware
1617
/// <summary>
1718
/// Can be overwritten to help you out during testing
1819
///
19-
/// This sets all necessary paaramters relating to the HttpContext for JADNC
20+
/// This sets all necessary parameters relating to the HttpContext for JADNC
2021
/// </summary>
2122
public class RequestMiddleware
2223
{
2324
private readonly RequestDelegate _next;
2425
private IResourceGraph _resourceGraph;
2526
private HttpContext _httpContext;
26-
private IJsonApiContext _jsonApiContext;
2727
private IRequestManager _requestManager;
2828
private IPageManager _pageManager;
2929
private IQueryParser _queryParser;
@@ -40,10 +40,10 @@ public async Task Invoke(HttpContext httpContext,
4040
IRequestManager requestManager,
4141
IPageManager pageManager,
4242
IQueryParser queryParser,
43-
IJsonApiOptions options)
43+
IJsonApiOptions options
44+
)
4445
{
4546
_httpContext = httpContext;
46-
_jsonApiContext = jsonApiContext;
4747
_resourceGraph = resourceGraph;
4848
_requestManager = requestManager;
4949
_pageManager = pageManager;
@@ -58,100 +58,10 @@ public async Task Invoke(HttpContext httpContext,
5858
// since the JsonApiContext is using field initializers
5959
// Need to work on finding a better solution.
6060
jsonApiContext.BeginOperation();
61-
ContextEntity contextEntityCurrent = GetCurrentEntity();
62-
// the contextEntity is null eg when we're using a non-JsonApiDotNetCore route.
63-
if (contextEntityCurrent != null)
64-
{
65-
requestManager.SetContextEntity(contextEntityCurrent);
66-
// TODO: this does not need to be reset every request: we shouldn't need to rely on an external request to figure out the basepath of current application
67-
requestManager.BasePath = GetBasePath(contextEntityCurrent.EntityName);
68-
//Handle all querySet
69-
HandleUriParameters();
70-
requestManager.IsRelationshipPath = PathIsRelationship();
71-
// BACKWARD COMPATIBILITY for v4 will be removed in v5
72-
jsonApiContext.RequestManager = requestManager;
73-
jsonApiContext.PageManager = new PageManager(new LinkBuilder(options, requestManager), options, requestManager);
74-
}
7561

7662
await _next(httpContext);
7763
}
7864
}
79-
/// <summary>
80-
/// Parses the uri
81-
/// </summary>
82-
/// <param name="context"></param>
83-
/// <param name="requestManager"></param>
84-
protected void HandleUriParameters()
85-
{
86-
if (_httpContext.Request.Query.Count > 0)
87-
{
88-
//requestManager.FullQuerySet = context.Request.Query;
89-
var querySet = _queryParser.Parse(_httpContext.Request.Query);
90-
_requestManager.QuerySet = querySet; //this shouldn't be exposed
91-
_pageManager.PageSize = querySet.PageQuery.PageSize ?? _pageManager.PageSize;
92-
_pageManager.CurrentPage = querySet.PageQuery.PageOffset ?? _pageManager.CurrentPage;
93-
_requestManager.IncludedRelationships = _requestManager.QuerySet.IncludedRelationships;
94-
}
95-
}
96-
97-
protected bool PathIsRelationship()
98-
{
99-
var actionName = (string)_httpContext.GetRouteData().Values["action"];
100-
return actionName.ToLower().Contains("relationships");
101-
}
102-
private string GetBasePath(string entityName)
103-
{
104-
var r = _httpContext.Request;
105-
if (_options.RelativeLinks)
106-
{
107-
return GetNamespaceFromPath(r.Path, entityName);
108-
}
109-
else
110-
{
111-
return $"{r.Scheme}://{r.Host}{GetNamespaceFromPath(r.Path, entityName)}";
112-
}
113-
}
114-
internal static string GetNamespaceFromPath(string path, string entityName)
115-
{
116-
var entityNameSpan = entityName.AsSpan();
117-
var pathSpan = path.AsSpan();
118-
const char delimiter = '/';
119-
for (var i = 0; i < pathSpan.Length; i++)
120-
{
121-
if (pathSpan[i].Equals(delimiter))
122-
{
123-
var nextPosition = i + 1;
124-
if (pathSpan.Length > i + entityNameSpan.Length)
125-
{
126-
var possiblePathSegment = pathSpan.Slice(nextPosition, entityNameSpan.Length);
127-
if (entityNameSpan.SequenceEqual(possiblePathSegment))
128-
{
129-
// check to see if it's the last position in the string
130-
// or if the next character is a /
131-
var lastCharacterPosition = nextPosition + entityNameSpan.Length;
132-
133-
if (lastCharacterPosition == pathSpan.Length || pathSpan.Length >= lastCharacterPosition + 2 && pathSpan[lastCharacterPosition].Equals(delimiter))
134-
{
135-
return pathSpan.Slice(0, i).ToString();
136-
}
137-
}
138-
}
139-
}
140-
}
141-
142-
return string.Empty;
143-
}
144-
/// <summary>
145-
/// Gets the current entity that we need for serialization and deserialization.
146-
/// </summary>
147-
/// <param name="context"></param>
148-
/// <param name="resourceGraph"></param>
149-
/// <returns></returns>
150-
private ContextEntity GetCurrentEntity()
151-
{
152-
var controllerName = (string)_httpContext.GetRouteData().Values["controller"];
153-
return _resourceGraph.GetEntityFromControllerName(controllerName);
154-
}
15565

15666
private bool IsValid()
15767
{

test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/QueryParameters.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,13 @@ public async Task Server_Returns_400_ForUnknownQueryParam()
3535

3636
// act
3737
var response = await client.SendAsync(request);
38-
var body = JsonConvert.DeserializeObject<ErrorCollection>(await response.Content.ReadAsStringAsync());
38+
var body = await response.Content.ReadAsStringAsync();
39+
var errorCollection = JsonConvert.DeserializeObject<ErrorCollection>(body);
3940

4041
// assert
4142
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
42-
Assert.Single(body.Errors);
43-
Assert.Equal($"[{queryKey}, {queryValue}] is not a valid query.", body.Errors[0].Title);
43+
Assert.Single(errorCollection.Errors);
44+
Assert.Equal($"[{queryKey}, {queryValue}] is not a valid query.", errorCollection.Errors[0].Title);
4445
}
4546
}
4647
}

0 commit comments

Comments
 (0)